From 048249edec843d3d6cc85e14ab797bfac3d1878c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:58:50 -0300 Subject: [PATCH 01/35] feat: integrate pdf-signature-validator for native validation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 13 +- composer.lock | 313 ++++++++++++++++++ lib/Handler/SignEngine/Pkcs12Handler.php | 234 +++++-------- .../PdfSignatureValidationService.php | 253 ++++++++++++++ .../Handler/SignEngine/Pkcs12HandlerTest.php | 94 +++++- .../PdfSignatureValidationServiceTest.php | 62 ++++ 6 files changed, 806 insertions(+), 163 deletions(-) create mode 100644 lib/Service/Signature/PdfSignatureValidationService.php create mode 100644 tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php diff --git a/composer.json b/composer.json index a0e68d0fda..c8cf094fed 100644 --- a/composer.json +++ b/composer.json @@ -1,4 +1,13 @@ { + "repositories": [ + { + "type": "path", + "url": "../pdf-signature-validator", + "options": { + "symlink": true + } + } + ], "require-dev": { "bamarni/composer-bin-plugin": "^1.8", "nextcloud/ocp": "dev-stable33", @@ -61,6 +70,8 @@ }, "require": { "cweagans/composer-patches": "^2.0", - "jeidison/signer-php": "dev-main" + "jeidison/signer-php": "dev-main", + "libresign/pdf-signature-validator": "*@dev", + "phpseclib/phpseclib": "^3.0" } } diff --git a/composer.lock b/composer.lock index 5e60c2a576..0407a59725 100644 --- a/composer.lock +++ b/composer.lock @@ -198,6 +198,318 @@ "source": "https://github.com/jeidison/signer-php/tree/main" }, "time": "2026-05-27T18:35:28+00:00" + }, + { + "name": "libresign/pdf-signature-validator", + "version": "dev-backport/7525/stable33-manual", + "dist": { + "type": "path", + "url": "../pdf-signature-validator", + "reference": "f8eba5ac5e43315f3856645e46e95eb21be4f742" + }, + "require": { + "php": "^8.2", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + } + }, + "autoload": { + "psr-4": { + "LibreSign\\PdfSignatureValidator\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "LibreSign\\PdfSignatureValidator\\Tests\\": "tests/" + } + }, + "scripts": { + "bin": [ + "echo 'bin not installed'" + ], + "lint": [ + "find src tests -name '*.php' -print0 | xargs -0 -n1 php -l" + ], + "cs:check": [ + "php-cs-fixer fix --dry-run --diff" + ], + "cs:fix": [ + "php-cs-fixer fix" + ], + "phpmd": [ + "phpmd src,tests text phpmd.xml" + ], + "psalm": [ + "psalm --no-cache --threads=$(nproc)" + ], + "rector:check": [ + "rector --dry-run" + ], + "rector:fix": [ + "rector" + ], + "test:unit": [ + "phpunit --colors=always --fail-on-warning --fail-on-risky" + ], + "test:coverage": [ + "XDEBUG_MODE=coverage phpunit --coverage-text" + ], + "test:mutation": [ + "infection --threads=$(nproc) --skip-initial-tests" + ], + "post-install-cmd": [ + "@composer bin all install --ansi" + ], + "post-update-cmd": [ + "@composer bin all install --ansi" + ] + }, + "license": [ + "AGPL-3.0-or-later" + ], + "description": "High-quality PDF signature extraction and validation primitives for LibreSign and external consumers.", + "transport-options": { + "symlink": true, + "relative": true + } + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" } ], "packages-dev": [ @@ -1614,6 +1926,7 @@ "minimum-stability": "stable", "stability-flags": { "jeidison/signer-php": 20, + "libresign/pdf-signature-validator": 20, "nextcloud/ocp": 20, "roave/security-advisories": 20 }, diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index df79acb39e..1be6640f6e 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -3,12 +3,12 @@ declare(strict_types=1); /** * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Libresign\Handler\SignEngine; -use DateTime; +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; @@ -18,17 +18,16 @@ use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; +use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Vendor\phpseclib3\File\ASN1; use OCP\Files\File; use OCP\IAppConfig; use OCP\IL10N; -use OCP\ITempManager; use Psr\Log\LoggerInterface; class Pkcs12Handler extends SignEngineHandler { use OrderCertificatesTrait; protected string $certificate = ''; - private array $signaturesFromPoppler = []; private ?JSignPdfHandler $jSignPdfHandler = null; private ?PhpNativeHandler $phpNativeHandler = null; private string $rootCertificatePem = ''; @@ -40,11 +39,12 @@ public function __construct( protected CertificateEngineFactory $certificateEngineFactory, private IL10N $l10n, private FooterHandler $footerHandler, - private ITempManager $tempManager, private LoggerInterface $logger, private CaIdentifierService $caIdentifierService, private DocMdpHandler $docMdpHandler, private CrlService $crlService, + private PdfSignatureValidationService $pdfSignatureValidationService, + private PdfSignatureExtractor $pdfSignatureExtractor, ) { parent::__construct($l10n, $folderService, $logger); } @@ -109,11 +109,14 @@ public function getCertificateChain($resource): array { return $certificates; } - private function processSignature($resource, ?string $signature): array { + private function processSignature($resource, ?string $signature, array $metadata = [], array $validation = []): array { $result = []; if (!$signature) { - $result['chain'][0]['signature_validation'] = $this->getReadableSigState('Digest Mismatch.'); + $result['chain'][0]['signature_validation'] = [ + 'id' => 3, + 'label' => $this->l10n->t('Digest mismatch.'), + ]; return $result; } @@ -123,7 +126,7 @@ private function processSignature($resource, ?string $signature): array { $chain = $this->extractCertificateChain($signature); if (!empty($chain)) { $result['chain'] = $this->orderCertificates($chain); - $result = $this->enrichLeafWithPopplerData($resource, $result); + $result = $this->enrichLeafWithNativeData($result, $metadata, $validation); } $result = $this->extractDocMdpData($resource, $result); @@ -275,149 +278,57 @@ private function getRootCertificatePem(): string { return $this->rootCertificatePem; } - private function enrichLeafWithPopplerData($resource, array $result): array { + private function enrichLeafWithNativeData(array $result, array $metadata, array $validation): array { if (empty($result['chain'])) { return $result; } - $popplerOnlyFields = ['field', 'range', 'certificate_validation']; - if (!isset($result['chain'][0]['subject'])) { - return $result; - } - $needPoppler = false; - foreach ($popplerOnlyFields as $field) { - if (empty($result['chain'][0][$field])) { - $needPoppler = true; - break; - } - } - if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) { - $needPoppler = true; - } - if (!$needPoppler) { - return $result; - } - $popplerChain = $this->chainFromPoppler($result['chain'][0]['subject'], $resource); - if (empty($popplerChain)) { - return $result; - } - foreach ($popplerOnlyFields as $field) { - if (isset($popplerChain[$field])) { - $result['chain'][0][$field] = $popplerChain[$field]; - } - } - if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) { - if (isset($popplerChain['signature_validation'])) { - $result['chain'][0]['signature_validation'] = $popplerChain['signature_validation']; - } - } - return $result; - } + $leaf = &$result['chain'][0]; - private function chainFromPoppler(array $subject, $resource): array { - $fromFallback = $this->popplerUtilsPdfSignFallback($resource); - foreach ($fromFallback as $popplerSig) { - if (!isset($popplerSig['chain'][0]['subject'])) { - continue; - } - if ($popplerSig['chain'][0]['subject'] == $subject) { - return $popplerSig['chain'][0]; + foreach (['field', 'range', 'signature_type', 'signing_hash_algorithm', 'covers_entire_document'] as $key) { + if (array_key_exists($key, $metadata)) { + $leaf[$key] = $metadata[$key]; } } - return []; - } - private function popplerUtilsPdfSignFallback($resource): array { - if (!empty($this->signaturesFromPoppler)) { - return $this->signaturesFromPoppler; - } - if (shell_exec('which pdfsig') === null) { - return $this->signaturesFromPoppler; + if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { + $leaf['signature_validation'] = $this->getLocalizedSignatureValidation($validation['signatureValidation']); } - rewind($resource); - $content = stream_get_contents($resource); - $tempFile = $this->tempManager->getTemporaryFile('file.pdf'); - file_put_contents($tempFile, $content); - $content = shell_exec('env TZ=UTC pdfsig ' . $tempFile); - if (empty($content)) { - return $this->signaturesFromPoppler; + if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { + $leaf['certificate_validation'] = $this->getLocalizedCertificateValidation($validation['certificateValidation']); } - $lines = explode("\n", $content); - $lastSignature = 0; - foreach ($lines as $item) { - $isFirstLevel = preg_match('/^Signature\s#(\d)/', $item, $match); - if ($isFirstLevel) { - $lastSignature = (int)$match[1] - 1; - $this->signaturesFromPoppler[$lastSignature] = []; - continue; - } - - $match = []; - $isSecondLevel = preg_match('/^\s+-\s(?.+):\s(?.*)/', $item, $match); - if ($isSecondLevel) { - switch ((string)$match['key']) { - case 'Signing Time': - $this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC')); - break; - case 'Signer full Distinguished Name': - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']); - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['name'] = $match['value']; - break; - case 'Signing Hash Algorithm': - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signatureTypeSN'] = $match['value']; - break; - case 'Signature Validation': - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signature_validation'] = $this->getReadableSigState($match['value']); - break; - case 'Certificate Validation': - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['certificate_validation'] = $this->getReadableCertState($match['value']); - break; - case 'Signed Ranges': - if (preg_match('/\[(\d+) - (\d+)\], \[(\d+) - (\d+)\]/', $match['value'], $ranges)) { - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['range'] = [ - 'offset1' => (int)$ranges[1], - 'length1' => (int)$ranges[2], - 'offset2' => (int)$ranges[3], - 'length2' => (int)$ranges[4], - ]; - } - break; - case 'Signature Field Name': - $this->signaturesFromPoppler[$lastSignature]['chain'][0]['field'] = $match['value']; - break; - case 'Signature Validation': - case 'Signature Type': - case 'Total document signed': - case 'Not total document signed': - default: - break; - } - } + if (!isset($leaf['certificate_validation'])) { + $leaf['certificate_validation'] = [ + 'id' => 3, + 'label' => $this->l10n->t('Certificate issuer is unknown.'), + ]; } - return $this->signaturesFromPoppler; + + return $result; } - private function getReadableSigState(string $status) { - return match ($status) { - 'Signature is Valid.' => [ + /** + * Keep LibreSign-side l10n compatibility regardless of upstream validator labels. + */ + private function getLocalizedSignatureValidation(array $validation): array { + $id = (int)($validation['id'] ?? 6); + + return match ($id) { + 1 => [ 'id' => 1, 'label' => $this->l10n->t('Signature is valid.'), ], - 'Signature is Invalid.' => [ + 2 => [ 'id' => 2, 'label' => $this->l10n->t('Signature is invalid.'), ], - 'Digest Mismatch.' => [ + 3 => [ 'id' => 3, 'label' => $this->l10n->t('Digest mismatch.'), ], - "Document isn't signed or corrupted data." => [ - 'id' => 4, - 'label' => $this->l10n->t("Document isn't signed or corrupted data."), - ], - 'Signature has not yet been verified.' => [ + 5 => [ 'id' => 5, 'label' => $this->l10n->t('Signature has not yet been verified.'), ], @@ -428,63 +339,72 @@ private function getReadableSigState(string $status) { }; } - private function getReadableCertState(string $status) { - return match ($status) { - 'Certificate is Trusted.' => [ + /** + * Keep LibreSign-side l10n compatibility regardless of upstream validator labels. + */ + private function getLocalizedCertificateValidation(array $validation): array { + $id = (int)($validation['id'] ?? 7); + + return match ($id) { + 1 => [ 'id' => 1, 'label' => $this->l10n->t('Certificate is trusted.'), ], - "Certificate issuer isn't Trusted." => [ + 2 => [ 'id' => 2, 'label' => $this->l10n->t("Certificate issuer isn't trusted."), ], - 'Certificate issuer is unknown.' => [ + 3 => [ 'id' => 3, 'label' => $this->l10n->t('Certificate issuer is unknown.'), ], - 'Certificate has been Revoked.' => [ + 4 => [ 'id' => 4, 'label' => $this->l10n->t('Certificate has been revoked.'), ], - 'Certificate has Expired' => [ + 5 => [ 'id' => 5, 'label' => $this->l10n->t('Certificate has expired'), ], - 'Certificate has not yet been verified.' => [ + 6 => [ 'id' => 6, 'label' => $this->l10n->t('Certificate has not yet been verified.'), ], default => [ 'id' => 7, - 'label' => $this->l10n->t('Unknown issue with Certificate or corrupted data.') + 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), ], }; } - private function parseDistinguishedNameWithMultipleValues(string $dn): array { - $result = []; - $pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn); + /** + * @param resource $resource + * @return list + */ + private function extractNativeSignatureMetadata($resource): array { + rewind($resource); + $content = stream_get_contents($resource); + if (!is_string($content) || $content === '') { + return []; + } - foreach ($pairs as $pair) { - [$key, $value] = explode('=', $pair, 2); - if (empty($key) || empty($value)) { - return $result; - } - $key = trim($key); - $value = trim($value); - $value = trim($value, '"'); - - if (!isset($result[$key])) { - $result[$key] = $value; - } else { - if (!is_array($result[$key])) { - $result[$key] = [$result[$key]]; - } - $result[$key][] = $value; - } + try { + $signatures = $this->pdfSignatureExtractor->extractFromString($content); + } catch (\Throwable) { + return []; } + $metadata = []; - return $result; + foreach ($signatures as $index => $signature) { + $metadata[$index] = [ + 'field' => $signature->metadata->field, + 'range' => $signature->metadata->range, + 'signature_type' => $signature->metadata->signatureType, + 'covers_entire_document' => $signature->metadata->coversEntireDocument, + ]; + } + + return $metadata; } private function der2pem($derData) { diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php new file mode 100644 index 0000000000..c90d62741b --- /dev/null +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -0,0 +1,253 @@ +validator = new PdfSignatureValidator(); + $this->loadLibreSignCaCertificate(); + } + + /** + * Load LibreSign CA certificate and set it as trusted root. + */ + private function loadLibreSignCaCertificate(): void { + // Try to load from config path first + $configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path'); + if (!empty($configPath) && is_dir($configPath)) { + $caPemPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem'; + if (is_readable($caPemPath)) { + $cert = @file_get_contents($caPemPath); + if ($cert !== false) { + $this->libresignCaCertificate = $cert; + $this->validator->addTrustedRoot($cert); + return; + } + } + } + + // Try alternate location + $alternateConfig = $this->appConfig->getValueString( + Application::APP_ID, + 'libresign_ca_certificate' + ); + if (!empty($alternateConfig)) { + $this->libresignCaCertificate = $alternateConfig; + $this->validator->addTrustedRoot($alternateConfig); + } + } + + /** + * Add additional trusted root certificate. + */ + public function addTrustedRoot(string $certificatePem): void { + $this->validator->addTrustedRoot($certificatePem); + } + + /** + * Set multiple trusted root certificates. + * + * @param list $certificates + */ + public function setTrustedRoots(array $certificates): void { + $this->validator->setTrustedRoots($certificates); + } + + /** + * Validate PDF signatures from file resource. + * + * @param resource $resource PDF file resource + * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) + * @return list + */ + public function validateFromResource($resource, ?DateTime $signatureTime = null): array { + try { + $results = $this->validator->validateFromResource($resource); + return $this->mapValidationResults($results, $signatureTime); + } catch (\Throwable $e) { + $this->logger->warning('PDF signature validation failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return []; + } + } + + /** + * Validate PDF signatures from binary content. + * + * @param string $pdfContent Binary PDF content + * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) + * @return list + */ + public function validateFromString(string $pdfContent, ?DateTime $signatureTime = null): array { + try { + $results = $this->validator->validateFromString($pdfContent); + return $this->mapValidationResults($results, $signatureTime); + } catch (\Throwable $e) { + $this->logger->warning('PDF signature validation failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return []; + } + } + + /** + * Map validation results from PdfSignatureValidator to LibreSign format. + * + * @param list $results Results from PdfSignatureValidator + * @param ?\DateTime $signatureTime + * @return list + */ + private function mapValidationResults(array $results, ?DateTime $signatureTime = null): array { + $mapped = []; + + foreach ($results as $result) { + $sigValidation = $result['signatureValidation'] ?? null; + $certValidation = $result['certificateValidation'] ?? null; + + if (!$sigValidation instanceof ValidationResult || !$certValidation instanceof ValidationResult) { + continue; + } + + $mapped[] = [ + 'signatureValidation' => $this->mapSignatureValidation($sigValidation), + 'certificateValidation' => $this->mapCertificateValidation($certValidation), + 'raw' => [ + 'signature' => $sigValidation, + 'certificate' => $certValidation, + ], + ]; + } + + return $mapped; + } + + /** + * Map signature validation result to LibreSign format. + */ + private function mapSignatureValidation(ValidationResult $result): array { + return match ($result->state) { + ValidationState::SIGNATURE_VALID => [ + 'id' => 1, + 'label' => 'Signature is valid.', + 'isValid' => true, + ], + ValidationState::SIGNATURE_INVALID => [ + 'id' => 2, + 'label' => 'Signature is invalid.', + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::DIGEST_MISMATCH => [ + 'id' => 3, + 'label' => 'Digest mismatch.', + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::NOT_VERIFIED => [ + 'id' => 5, + 'label' => 'Signature has not yet been verified.', + 'reason' => $result->reason, + 'isValid' => false, + ], + default => [ + 'id' => 6, + 'label' => 'Unknown validation failure.', + 'reason' => $result->reason, + 'isValid' => false, + ], + }; + } + + /** + * Map certificate validation result to LibreSign format. + */ + private function mapCertificateValidation(ValidationResult $result): array { + return match ($result->state) { + ValidationState::CERT_TRUSTED => [ + 'id' => 1, + 'label' => 'Certificate is trusted.', + 'isValid' => true, + ], + ValidationState::CERT_ISSUER_NOT_TRUSTED => [ + 'id' => 2, + 'label' => "Certificate issuer isn't trusted.", + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::CERT_ISSUER_UNKNOWN => [ + 'id' => 3, + 'label' => 'Certificate issuer is unknown.', + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::CERT_REVOKED => [ + 'id' => 4, + 'label' => 'Certificate has been revoked.', + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::CERT_EXPIRED => [ + 'id' => 5, + 'label' => 'Certificate has expired.', + 'reason' => $result->reason, + 'isValid' => false, + ], + ValidationState::CERT_NOT_VERIFIED => [ + 'id' => 6, + 'label' => 'Certificate has not yet been verified.', + 'reason' => $result->reason, + 'isValid' => false, + ], + default => [ + 'id' => 7, + 'label' => 'Unknown issue with certificate or corrupted data.', + 'reason' => $result->reason, + 'isValid' => false, + ], + }; + } + + /** + * Check if LibreSign CA is loaded. + */ + public function isLibreSignCaLoaded(): bool { + return !empty($this->libresignCaCertificate); + } + + /** + * Get LibreSign CA certificate (PEM format). + */ + public function getLibreSignCaCertificate(): string { + return $this->libresignCaCertificate; + } +} diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index b1e53160e0..11a5f15b20 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -9,6 +9,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\DocMdpHandler; @@ -17,12 +18,12 @@ use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; +use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAppConfig; use OCP\IL10N; -use OCP\ITempManager; use OCP\L10N\IFactory as IL10NFactory; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; @@ -34,12 +35,13 @@ final class Pkcs12HandlerTest extends \OCA\Libresign\Tests\Unit\TestCase { private IAppConfig $appConfig; private IL10N $l10n; private FooterHandler&MockObject $footerHandler; - private ITempManager $tempManager; private LoggerInterface&MockObject $logger; private CertificateEngineFactory&MockObject $certificateEngineFactory; private CaIdentifierService&MockObject $caIdentifierService; private DocMdpHandler&MockObject $docMdpHandler; private CrlService&MockObject $crlService; + private PdfSignatureValidationService&MockObject $pdfSignatureValidationService; + private PdfSignatureExtractor $pdfSignatureExtractor; public function setUp(): void { $this->folderService = $this->createMock(FolderService::class); @@ -47,11 +49,13 @@ public function setUp(): void { $this->certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); $this->l10n = \OCP\Server::get(IL10NFactory::class)->get(Application::APP_ID); $this->footerHandler = $this->createMock(FooterHandler::class); - $this->tempManager = \OCP\Server::get(ITempManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->caIdentifierService = $this->createMock(CaIdentifierService::class); $this->docMdpHandler = $this->createMock(DocMdpHandler::class); $this->crlService = $this->createMock(CrlService::class); + $this->pdfSignatureValidationService = $this->createMock(PdfSignatureValidationService::class); + $this->pdfSignatureValidationService->method('validateFromResource')->willReturn([]); + $this->pdfSignatureExtractor = new PdfSignatureExtractor(); } private function getHandler(array $methods = []): Pkcs12Handler|MockObject { @@ -63,11 +67,12 @@ private function getHandler(array $methods = []): Pkcs12Handler|MockObject { $this->certificateEngineFactory, $this->l10n, $this->footerHandler, - $this->tempManager, $this->logger, $this->caIdentifierService, $this->docMdpHandler, $this->crlService, + $this->pdfSignatureValidationService, + $this->pdfSignatureExtractor, ]) ->onlyMethods($methods) ->getMock(); @@ -78,11 +83,12 @@ private function getHandler(array $methods = []): Pkcs12Handler|MockObject { $this->certificateEngineFactory, $this->l10n, $this->footerHandler, - $this->tempManager, $this->logger, $this->caIdentifierService, $this->docMdpHandler, $this->crlService, + $this->pdfSignatureValidationService, + $this->pdfSignatureExtractor, ); } @@ -403,4 +409,82 @@ public function testDocMdpPdfsExtraction(): void { } } + public function testPackageExtractorParsesFieldAndRange(): void { + $content = file_get_contents(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf'); + $this->assertIsString($content); + + $signatures = $this->pdfSignatureExtractor->extractFromString($content); + $this->assertCount(1, $signatures); + + $metadata = $signatures[0]->metadata; + $this->assertSame('Signature1', $metadata->field); + $this->assertSame([ + 'offset1' => 0, + 'length1' => 1311, + 'offset2' => 31313, + 'length2' => 32829, + ], $metadata->range); + } + + public function testGetCertificateChainProvidesNativePackageShape(): void { + $this->pdfSignatureValidationService->method('validateFromResource') + ->willReturn([ + [ + 'signatureValidation' => ['id' => 1, 'label' => 'Signature is valid.'], + 'certificateValidation' => ['id' => 3, 'label' => 'Certificate issuer is unknown.'], + ], + ]); + + $handler = $this->getHandler(); + $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); + $this->assertIsResource($resource); + + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertCount(1, $result); + $this->assertArrayHasKey('signingTime', $result[0]); + $this->assertInstanceOf(\DateTime::class, $result[0]['signingTime']); + + $this->assertArrayHasKey('chain', $result[0]); + $this->assertNotEmpty($result[0]['chain']); + + $leaf = $result[0]['chain'][0]; + $this->assertArrayHasKey('field', $leaf); + $this->assertEquals('Signature1', $leaf['field']); + $this->assertArrayHasKey('range', $leaf); + $this->assertSame([ + 'offset1' => 0, + 'length1' => 1311, + 'offset2' => 31313, + 'length2' => 32829, + ], $leaf['range']); + + $this->assertArrayHasKey('signature_validation', $leaf); + $this->assertEquals(1, $leaf['signature_validation']['id']); + + $this->assertArrayHasKey('certificate_validation', $leaf); + $this->assertSame(3, $leaf['certificate_validation']['id']); + $this->assertArrayHasKey('signature_type', $leaf); + $this->assertNotEmpty($leaf['signature_type']); + $this->assertArrayHasKey('covers_entire_document', $leaf); + $this->assertIsBool($leaf['covers_entire_document']); + } + + public function testGetCertificateChainUsesNativeValidationServiceForEachSignature(): void { + $this->pdfSignatureValidationService->expects($this->once()) + ->method('validateFromResource'); + + $handler = $this->getHandler(); + $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); + $this->assertIsResource($resource); + + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertNotEmpty($result); + $this->assertSame(1, $result[0]['chain'][0]['signature_validation']['id']); + $this->assertSame(3, $result[0]['chain'][0]['certificate_validation']['id']); + } + } diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php new file mode 100644 index 0000000000..28aa701023 --- /dev/null +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -0,0 +1,62 @@ +newServiceWithoutConstructor(); + $result = $this->invokePrivateMethod( + $service, + 'mapSignatureValidation', + new ValidationResult(ValidationState::DIGEST_MISMATCH, 'hash mismatch') + ); + + $this->assertSame(3, $result['id']); + $this->assertSame('Digest mismatch.', $result['label']); + $this->assertSame('hash mismatch', $result['reason']); + $this->assertFalse($result['isValid']); + } + + public function testMapCertificateValidationWithEnumState(): void { + $service = $this->newServiceWithoutConstructor(); + $result = $this->invokePrivateMethod( + $service, + 'mapCertificateValidation', + new ValidationResult(ValidationState::CERT_TRUSTED) + ); + + $this->assertSame(1, $result['id']); + $this->assertSame('Certificate is trusted.', $result['label']); + $this->assertTrue($result['isValid']); + } + + private function newServiceWithoutConstructor(): PdfSignatureValidationService { + $reflection = new \ReflectionClass(PdfSignatureValidationService::class); + /** @var PdfSignatureValidationService $service */ + $service = $reflection->newInstanceWithoutConstructor(); + return $service; + } + + /** + * @return array + */ + private function invokePrivateMethod(PdfSignatureValidationService $service, string $method, ValidationResult $result): array { + $reflection = new \ReflectionClass($service); + $target = $reflection->getMethod($method); + $target->setAccessible(true); + /** @var array $mapped */ + $mapped = $target->invoke($service, $result); + return $mapped; + } +} From 9db2e63d15f5bb19b9308b03f00e6761510b25c1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:00:59 -0300 Subject: [PATCH 02/35] refactor: translate signature validation messages via l10n Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 54 +++++++++++-------- .../PdfSignatureValidationServiceTest.php | 17 ++++++ 2 files changed, 49 insertions(+), 22 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index c90d62741b..a45498cb3a 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -14,6 +14,7 @@ use LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\AppInfo\Application; use OCP\IAppConfig; +use OCP\IL10N; use Psr\Log\LoggerInterface; /** @@ -29,6 +30,7 @@ class PdfSignatureValidationService { public function __construct( private IAppConfig $appConfig, + private IL10N $l10n, private LoggerInterface $logger, ) { $this->validator = new PdfSignatureValidator(); @@ -158,31 +160,31 @@ private function mapSignatureValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::SIGNATURE_VALID => [ 'id' => 1, - 'label' => 'Signature is valid.', + 'label' => $this->l10n->t('Signature is valid.'), 'isValid' => true, ], ValidationState::SIGNATURE_INVALID => [ 'id' => 2, - 'label' => 'Signature is invalid.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Signature is invalid.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::DIGEST_MISMATCH => [ 'id' => 3, - 'label' => 'Digest mismatch.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Digest mismatch.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::NOT_VERIFIED => [ 'id' => 5, - 'label' => 'Signature has not yet been verified.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Signature has not yet been verified.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], default => [ 'id' => 6, - 'label' => 'Unknown validation failure.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Unknown validation failure.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], }; @@ -195,48 +197,56 @@ private function mapCertificateValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::CERT_TRUSTED => [ 'id' => 1, - 'label' => 'Certificate is trusted.', + 'label' => $this->l10n->t('Certificate is trusted.'), 'isValid' => true, ], ValidationState::CERT_ISSUER_NOT_TRUSTED => [ 'id' => 2, - 'label' => "Certificate issuer isn't trusted.", - 'reason' => $result->reason, + 'label' => $this->l10n->t("Certificate issuer isn't trusted."), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::CERT_ISSUER_UNKNOWN => [ 'id' => 3, - 'label' => 'Certificate issuer is unknown.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Certificate issuer is unknown.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::CERT_REVOKED => [ 'id' => 4, - 'label' => 'Certificate has been revoked.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Certificate has been revoked.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::CERT_EXPIRED => [ 'id' => 5, - 'label' => 'Certificate has expired.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Certificate has expired.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], ValidationState::CERT_NOT_VERIFIED => [ 'id' => 6, - 'label' => 'Certificate has not yet been verified.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Certificate has not yet been verified.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], default => [ 'id' => 7, - 'label' => 'Unknown issue with certificate or corrupted data.', - 'reason' => $result->reason, + 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), + 'reason' => $this->translateReason($result->reason), 'isValid' => false, ], }; } + private function translateReason(?string $reason): ?string { + if ($reason === null || $reason === '') { + return $reason; + } + + return $this->l10n->t($reason); + } + /** * Check if LibreSign CA is loaded. */ diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index 28aa701023..1b5a8ee408 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -12,8 +12,20 @@ use LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Unit\TestCase; +use OCP\IL10N; +use PHPUnit\Framework\MockObject\MockObject; final class PdfSignatureValidationServiceTest extends TestCase { + private IL10N&MockObject $l10n; + + public function setUp(): void { + parent::setUp(); + $this->l10n = $this->createMock(IL10N::class); + $this->l10n + ->method('t') + ->willReturnCallback(static fn (string $text): string => $text); + } + public function testMapSignatureValidationWithEnumState(): void { $service = $this->newServiceWithoutConstructor(); $result = $this->invokePrivateMethod( @@ -45,6 +57,11 @@ private function newServiceWithoutConstructor(): PdfSignatureValidationService { $reflection = new \ReflectionClass(PdfSignatureValidationService::class); /** @var PdfSignatureValidationService $service */ $service = $reflection->newInstanceWithoutConstructor(); + + $l10nProperty = $reflection->getProperty('l10n'); + $l10nProperty->setAccessible(true); + $l10nProperty->setValue($service, $this->l10n); + return $service; } From 9d4686ba6641e3a008cdd95ddf8edcf29b90b537 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:01:59 -0300 Subject: [PATCH 03/35] refactor: avoid translating dynamic reason strings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index a45498cb3a..8a81bccd5b 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -166,25 +166,25 @@ private function mapSignatureValidation(ValidationResult $result): array { ValidationState::SIGNATURE_INVALID => [ 'id' => 2, 'label' => $this->l10n->t('Signature is invalid.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::DIGEST_MISMATCH => [ 'id' => 3, 'label' => $this->l10n->t('Digest mismatch.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::NOT_VERIFIED => [ 'id' => 5, 'label' => $this->l10n->t('Signature has not yet been verified.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], default => [ 'id' => 6, 'label' => $this->l10n->t('Unknown validation failure.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], }; @@ -203,50 +203,42 @@ private function mapCertificateValidation(ValidationResult $result): array { ValidationState::CERT_ISSUER_NOT_TRUSTED => [ 'id' => 2, 'label' => $this->l10n->t("Certificate issuer isn't trusted."), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::CERT_ISSUER_UNKNOWN => [ 'id' => 3, 'label' => $this->l10n->t('Certificate issuer is unknown.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::CERT_REVOKED => [ 'id' => 4, 'label' => $this->l10n->t('Certificate has been revoked.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::CERT_EXPIRED => [ 'id' => 5, 'label' => $this->l10n->t('Certificate has expired.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], ValidationState::CERT_NOT_VERIFIED => [ 'id' => 6, 'label' => $this->l10n->t('Certificate has not yet been verified.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], default => [ 'id' => 7, 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), - 'reason' => $this->translateReason($result->reason), + 'reason' => $result->reason, 'isValid' => false, ], }; } - private function translateReason(?string $reason): ?string { - if ($reason === null || $reason === '') { - return $reason; - } - - return $this->l10n->t($reason); - } - /** * Check if LibreSign CA is loaded. */ From 38623a6f0553d1f80180e8f3c7c98be6a7520570 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:03:58 -0300 Subject: [PATCH 04/35] feat: add translatable dictionary for validation reasons Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 64 ++++++++++++++++--- .../PdfSignatureValidationServiceTest.php | 22 +++++++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 8a81bccd5b..ad3a4e0c43 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -166,25 +166,25 @@ private function mapSignatureValidation(ValidationResult $result): array { ValidationState::SIGNATURE_INVALID => [ 'id' => 2, 'label' => $this->l10n->t('Signature is invalid.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::DIGEST_MISMATCH => [ 'id' => 3, 'label' => $this->l10n->t('Digest mismatch.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::NOT_VERIFIED => [ 'id' => 5, 'label' => $this->l10n->t('Signature has not yet been verified.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 6, 'label' => $this->l10n->t('Unknown validation failure.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], }; @@ -203,42 +203,86 @@ private function mapCertificateValidation(ValidationResult $result): array { ValidationState::CERT_ISSUER_NOT_TRUSTED => [ 'id' => 2, 'label' => $this->l10n->t("Certificate issuer isn't trusted."), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_ISSUER_UNKNOWN => [ 'id' => 3, 'label' => $this->l10n->t('Certificate issuer is unknown.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_REVOKED => [ 'id' => 4, 'label' => $this->l10n->t('Certificate has been revoked.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_EXPIRED => [ 'id' => 5, 'label' => $this->l10n->t('Certificate has expired.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_NOT_VERIFIED => [ 'id' => 6, 'label' => $this->l10n->t('Certificate has not yet been verified.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 7, 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), - 'reason' => $result->reason, + 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], }; } + private function translateKnownReason(?string $reason): ?string { + if ($reason === null || $reason === '') { + return $reason; + } + + if (preg_match('/^Intermediate certificate at position (\d+) is not signed by issuer$/', $reason, $matches) === 1) { + return $this->l10n->t( + 'Intermediate certificate at position %s is not signed by issuer', + [$matches[1]] + ); + } + + $prefix = 'Certificate validation failed: '; + if (str_starts_with($reason, $prefix)) { + $detail = substr($reason, strlen($prefix)); + $translatedDetail = $this->translateKnownReason($detail) ?? $detail; + return $this->l10n->t('Certificate validation failed: %s', [$translatedDetail]); + } + + return match ($reason) { + 'No ByteRange in signature' => $this->l10n->t('No ByteRange in signature'), + 'PDF content hash does not match signed digest' => $this->l10n->t('PDF content hash does not match signed digest'), + 'Signature does not match certificate' => $this->l10n->t('Signature does not match certificate'), + 'Failed to parse certificate' => $this->l10n->t('Failed to parse certificate'), + 'Certificate was not valid at time of signature' => $this->l10n->t('Certificate was not valid at time of signature'), + 'Certificate has expired' => $this->l10n->t('Certificate has expired'), + 'Empty certificate chain' => $this->l10n->t('Empty certificate chain'), + 'Certificate has no serial number' => $this->l10n->t('Certificate has no serial number'), + 'Certificate found in CRL' => $this->l10n->t('Certificate found in CRL'), + 'Invalid certificate' => $this->l10n->t('Invalid certificate'), + 'Leaf certificate is marked as CA' => $this->l10n->t('Leaf certificate is marked as CA'), + 'Certificate signature validation failed' => $this->l10n->t('Certificate signature validation failed'), + 'Self-signed certificate not in trusted roots' => $this->l10n->t('Self-signed certificate not in trusted roots'), + 'Root certificate is not self-signed' => $this->l10n->t('Root certificate is not self-signed'), + 'Root certificate is not in trusted list' => $this->l10n->t('Root certificate is not in trusted list'), + 'No binary signature' => $this->l10n->t('No binary signature'), + 'No certificates in signature' => $this->l10n->t('No certificates in signature'), + 'Signing certificate has expired' => $this->l10n->t('Signing certificate has expired'), + 'Signing certificate has been revoked' => $this->l10n->t('Signing certificate has been revoked'), + 'Signature verification incomplete' => $this->l10n->t('Signature verification incomplete'), + default => $reason, + }; + } + /** * Check if LibreSign CA is loaded. */ diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index 1b5a8ee408..cdc30d5a10 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -53,6 +53,28 @@ public function testMapCertificateValidationWithEnumState(): void { $this->assertTrue($result['isValid']); } + public function testMapReasonUsesDictionaryForKnownReason(): void { + $service = $this->newServiceWithoutConstructor(); + $result = $this->invokePrivateMethod( + $service, + 'mapSignatureValidation', + new ValidationResult(ValidationState::DIGEST_MISMATCH, 'PDF content hash does not match signed digest') + ); + + $this->assertSame('PDF content hash does not match signed digest', $result['reason']); + } + + public function testMapReasonKeepsUnknownReasonUntouched(): void { + $service = $this->newServiceWithoutConstructor(); + $result = $this->invokePrivateMethod( + $service, + 'mapSignatureValidation', + new ValidationResult(ValidationState::DIGEST_MISMATCH, 'custom runtime detail') + ); + + $this->assertSame('custom runtime detail', $result['reason']); + } + private function newServiceWithoutConstructor(): PdfSignatureValidationService { $reflection = new \ReflectionClass(PdfSignatureValidationService::class); /** @var PdfSignatureValidationService $service */ From b9796c6ff474eca2084389f3889827e2e6034542 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:05:10 -0300 Subject: [PATCH 05/35] docs: add translator guidance for validation reasons Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/Signature/PdfSignatureValidationService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index ad3a4e0c43..cb6963bd3a 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -245,6 +245,7 @@ private function translateKnownReason(?string $reason): ?string { } if (preg_match('/^Intermediate certificate at position (\d+) is not signed by issuer$/', $reason, $matches) === 1) { + // TRANSLATORS: %s is the numeric position of an intermediate certificate in the chain. return $this->l10n->t( 'Intermediate certificate at position %s is not signed by issuer', [$matches[1]] @@ -255,9 +256,12 @@ private function translateKnownReason(?string $reason): ?string { if (str_starts_with($reason, $prefix)) { $detail = substr($reason, strlen($prefix)); $translatedDetail = $this->translateKnownReason($detail) ?? $detail; + // TRANSLATORS: %s is a translated certificate validation detail message. return $this->l10n->t('Certificate validation failed: %s', [$translatedDetail]); } + // TRANSLATORS: Technical validation reasons from pdf-signature-validator. + // Keep protocol/acronym terms like ByteRange, CRL and CA as-is. return match ($reason) { 'No ByteRange in signature' => $this->l10n->t('No ByteRange in signature'), 'PDF content hash does not match signed digest' => $this->l10n->t('PDF content hash does not match signed digest'), From dcd2ab558da3f423bb4cbd50274c442cbc67065a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:06:48 -0300 Subject: [PATCH 06/35] docs: place translator comments above each l10n call Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index cb6963bd3a..b92b4f072c 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -160,29 +160,34 @@ private function mapSignatureValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::SIGNATURE_VALID => [ 'id' => 1, + // TRANSLATORS: User-facing status when signature cryptographic validation succeeds. 'label' => $this->l10n->t('Signature is valid.'), 'isValid' => true, ], ValidationState::SIGNATURE_INVALID => [ 'id' => 2, + // TRANSLATORS: User-facing status when signature cryptographic validation fails. 'label' => $this->l10n->t('Signature is invalid.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::DIGEST_MISMATCH => [ 'id' => 3, + // TRANSLATORS: User-facing status when signed digest does not match PDF content. 'label' => $this->l10n->t('Digest mismatch.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::NOT_VERIFIED => [ 'id' => 5, + // TRANSLATORS: User-facing status when validation could not be fully completed. 'label' => $this->l10n->t('Signature has not yet been verified.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 6, + // TRANSLATORS: Generic fallback status for unexpected signature validation failures. 'label' => $this->l10n->t('Unknown validation failure.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, @@ -197,41 +202,48 @@ private function mapCertificateValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::CERT_TRUSTED => [ 'id' => 1, + // TRANSLATORS: User-facing status when certificate is trusted. 'label' => $this->l10n->t('Certificate is trusted.'), 'isValid' => true, ], ValidationState::CERT_ISSUER_NOT_TRUSTED => [ 'id' => 2, + // TRANSLATORS: User-facing status when issuing CA is known but not trusted. 'label' => $this->l10n->t("Certificate issuer isn't trusted."), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_ISSUER_UNKNOWN => [ 'id' => 3, + // TRANSLATORS: User-facing status when certificate issuer cannot be identified/trusted. 'label' => $this->l10n->t('Certificate issuer is unknown.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_REVOKED => [ 'id' => 4, + // TRANSLATORS: User-facing status when certificate is revoked. 'label' => $this->l10n->t('Certificate has been revoked.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_EXPIRED => [ 'id' => 5, + // TRANSLATORS: User-facing status when certificate is expired. 'label' => $this->l10n->t('Certificate has expired.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_NOT_VERIFIED => [ 'id' => 6, + // TRANSLATORS: User-facing status when certificate validation could not be completed. 'label' => $this->l10n->t('Certificate has not yet been verified.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 7, + // TRANSLATORS: Generic fallback status for unexpected certificate validation failures. 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, @@ -260,28 +272,46 @@ private function translateKnownReason(?string $reason): ?string { return $this->l10n->t('Certificate validation failed: %s', [$translatedDetail]); } - // TRANSLATORS: Technical validation reasons from pdf-signature-validator. - // Keep protocol/acronym terms like ByteRange, CRL and CA as-is. return match ($reason) { + // TRANSLATORS: Technical term from PDF signatures. Keep "ByteRange" unchanged. 'No ByteRange in signature' => $this->l10n->t('No ByteRange in signature'), + // TRANSLATORS: Technical message for digest/hash mismatch in PDF signature verification. 'PDF content hash does not match signed digest' => $this->l10n->t('PDF content hash does not match signed digest'), + // TRANSLATORS: Certificate/public-key verification failed for signature bytes. 'Signature does not match certificate' => $this->l10n->t('Signature does not match certificate'), + // TRANSLATORS: X.509 certificate parsing failure. 'Failed to parse certificate' => $this->l10n->t('Failed to parse certificate'), + // TRANSLATORS: Signature timestamp is outside certificate validity window. 'Certificate was not valid at time of signature' => $this->l10n->t('Certificate was not valid at time of signature'), + // TRANSLATORS: Certificate validity date has ended. 'Certificate has expired' => $this->l10n->t('Certificate has expired'), + // TRANSLATORS: No certificates were found in provided certificate chain. 'Empty certificate chain' => $this->l10n->t('Empty certificate chain'), + // TRANSLATORS: Certificate does not provide a serial number field. 'Certificate has no serial number' => $this->l10n->t('Certificate has no serial number'), + // TRANSLATORS: CRL means Certificate Revocation List; keep acronym CRL unchanged. 'Certificate found in CRL' => $this->l10n->t('Certificate found in CRL'), + // TRANSLATORS: Certificate structure/content is invalid. 'Invalid certificate' => $this->l10n->t('Invalid certificate'), + // TRANSLATORS: CA means Certificate Authority; keep acronym CA unchanged. 'Leaf certificate is marked as CA' => $this->l10n->t('Leaf certificate is marked as CA'), + // TRANSLATORS: Certificate signature chain validation failed. 'Certificate signature validation failed' => $this->l10n->t('Certificate signature validation failed'), + // TRANSLATORS: Self-signed certificate is not present in trusted roots list. 'Self-signed certificate not in trusted roots' => $this->l10n->t('Self-signed certificate not in trusted roots'), + // TRANSLATORS: Root certificate must be self-signed to be considered a trust anchor. 'Root certificate is not self-signed' => $this->l10n->t('Root certificate is not self-signed'), + // TRANSLATORS: Root certificate is not present in configured trusted certificate list. 'Root certificate is not in trusted list' => $this->l10n->t('Root certificate is not in trusted list'), + // TRANSLATORS: Signature container has no binary signature payload. 'No binary signature' => $this->l10n->t('No binary signature'), + // TRANSLATORS: Signature payload has no embedded certificates. 'No certificates in signature' => $this->l10n->t('No certificates in signature'), + // TRANSLATORS: Certificate used for signing is expired. 'Signing certificate has expired' => $this->l10n->t('Signing certificate has expired'), + // TRANSLATORS: Certificate used for signing is revoked. 'Signing certificate has been revoked' => $this->l10n->t('Signing certificate has been revoked'), + // TRANSLATORS: Signature verification could not be fully completed. 'Signature verification incomplete' => $this->l10n->t('Signature verification incomplete'), default => $reason, }; From dd098b673f5b50aa822018f0d78243c9e3b73779 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:07:42 -0300 Subject: [PATCH 07/35] docs: place translator comments above each l10n call Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index b92b4f072c..93bf9cff87 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -160,34 +160,34 @@ private function mapSignatureValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::SIGNATURE_VALID => [ 'id' => 1, - // TRANSLATORS: User-facing status when signature cryptographic validation succeeds. + // TRANSLATORS User-facing status when signature cryptographic validation succeeds. 'label' => $this->l10n->t('Signature is valid.'), 'isValid' => true, ], ValidationState::SIGNATURE_INVALID => [ 'id' => 2, - // TRANSLATORS: User-facing status when signature cryptographic validation fails. + // TRANSLATORS User-facing status when signature cryptographic validation fails. 'label' => $this->l10n->t('Signature is invalid.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::DIGEST_MISMATCH => [ 'id' => 3, - // TRANSLATORS: User-facing status when signed digest does not match PDF content. + // TRANSLATORS User-facing status when signed digest does not match PDF content. 'label' => $this->l10n->t('Digest mismatch.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::NOT_VERIFIED => [ 'id' => 5, - // TRANSLATORS: User-facing status when validation could not be fully completed. + // TRANSLATORS User-facing status when validation could not be fully completed. 'label' => $this->l10n->t('Signature has not yet been verified.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 6, - // TRANSLATORS: Generic fallback status for unexpected signature validation failures. + // TRANSLATORS Generic fallback status for unexpected signature validation failures. 'label' => $this->l10n->t('Unknown validation failure.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, @@ -202,48 +202,48 @@ private function mapCertificateValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::CERT_TRUSTED => [ 'id' => 1, - // TRANSLATORS: User-facing status when certificate is trusted. + // TRANSLATORS User-facing status when certificate is trusted. 'label' => $this->l10n->t('Certificate is trusted.'), 'isValid' => true, ], ValidationState::CERT_ISSUER_NOT_TRUSTED => [ 'id' => 2, - // TRANSLATORS: User-facing status when issuing CA is known but not trusted. + // TRANSLATORS User-facing status when issuing CA is known but not trusted. 'label' => $this->l10n->t("Certificate issuer isn't trusted."), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_ISSUER_UNKNOWN => [ 'id' => 3, - // TRANSLATORS: User-facing status when certificate issuer cannot be identified/trusted. + // TRANSLATORS User-facing status when certificate issuer cannot be identified/trusted. 'label' => $this->l10n->t('Certificate issuer is unknown.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_REVOKED => [ 'id' => 4, - // TRANSLATORS: User-facing status when certificate is revoked. + // TRANSLATORS User-facing status when certificate is revoked. 'label' => $this->l10n->t('Certificate has been revoked.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_EXPIRED => [ 'id' => 5, - // TRANSLATORS: User-facing status when certificate is expired. + // TRANSLATORS User-facing status when certificate is expired. 'label' => $this->l10n->t('Certificate has expired.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], ValidationState::CERT_NOT_VERIFIED => [ 'id' => 6, - // TRANSLATORS: User-facing status when certificate validation could not be completed. + // TRANSLATORS User-facing status when certificate validation could not be completed. 'label' => $this->l10n->t('Certificate has not yet been verified.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, ], default => [ 'id' => 7, - // TRANSLATORS: Generic fallback status for unexpected certificate validation failures. + // TRANSLATORS Generic fallback status for unexpected certificate validation failures. 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), 'reason' => $this->translateKnownReason($result->reason), 'isValid' => false, @@ -257,7 +257,7 @@ private function translateKnownReason(?string $reason): ?string { } if (preg_match('/^Intermediate certificate at position (\d+) is not signed by issuer$/', $reason, $matches) === 1) { - // TRANSLATORS: %s is the numeric position of an intermediate certificate in the chain. + // TRANSLATORS %s is the numeric position of an intermediate certificate in the chain. return $this->l10n->t( 'Intermediate certificate at position %s is not signed by issuer', [$matches[1]] @@ -268,50 +268,50 @@ private function translateKnownReason(?string $reason): ?string { if (str_starts_with($reason, $prefix)) { $detail = substr($reason, strlen($prefix)); $translatedDetail = $this->translateKnownReason($detail) ?? $detail; - // TRANSLATORS: %s is a translated certificate validation detail message. + // TRANSLATORS %s is a translated certificate validation detail message. return $this->l10n->t('Certificate validation failed: %s', [$translatedDetail]); } return match ($reason) { - // TRANSLATORS: Technical term from PDF signatures. Keep "ByteRange" unchanged. + // TRANSLATORS Technical term from PDF signatures. Keep "ByteRange" unchanged. 'No ByteRange in signature' => $this->l10n->t('No ByteRange in signature'), - // TRANSLATORS: Technical message for digest/hash mismatch in PDF signature verification. + // TRANSLATORS Technical message for digest/hash mismatch in PDF signature verification. 'PDF content hash does not match signed digest' => $this->l10n->t('PDF content hash does not match signed digest'), - // TRANSLATORS: Certificate/public-key verification failed for signature bytes. + // TRANSLATORS Certificate/public-key verification failed for signature bytes. 'Signature does not match certificate' => $this->l10n->t('Signature does not match certificate'), - // TRANSLATORS: X.509 certificate parsing failure. + // TRANSLATORS X.509 certificate parsing failure. 'Failed to parse certificate' => $this->l10n->t('Failed to parse certificate'), - // TRANSLATORS: Signature timestamp is outside certificate validity window. + // TRANSLATORS Signature timestamp is outside certificate validity window. 'Certificate was not valid at time of signature' => $this->l10n->t('Certificate was not valid at time of signature'), - // TRANSLATORS: Certificate validity date has ended. + // TRANSLATORS Certificate validity date has ended. 'Certificate has expired' => $this->l10n->t('Certificate has expired'), - // TRANSLATORS: No certificates were found in provided certificate chain. + // TRANSLATORS No certificates were found in provided certificate chain. 'Empty certificate chain' => $this->l10n->t('Empty certificate chain'), - // TRANSLATORS: Certificate does not provide a serial number field. + // TRANSLATORS Certificate does not provide a serial number field. 'Certificate has no serial number' => $this->l10n->t('Certificate has no serial number'), - // TRANSLATORS: CRL means Certificate Revocation List; keep acronym CRL unchanged. + // TRANSLATORS CRL means Certificate Revocation List; keep acronym CRL unchanged. 'Certificate found in CRL' => $this->l10n->t('Certificate found in CRL'), - // TRANSLATORS: Certificate structure/content is invalid. + // TRANSLATORS Certificate structure/content is invalid. 'Invalid certificate' => $this->l10n->t('Invalid certificate'), - // TRANSLATORS: CA means Certificate Authority; keep acronym CA unchanged. + // TRANSLATORS CA means Certificate Authority; keep acronym CA unchanged. 'Leaf certificate is marked as CA' => $this->l10n->t('Leaf certificate is marked as CA'), - // TRANSLATORS: Certificate signature chain validation failed. + // TRANSLATORS Certificate signature chain validation failed. 'Certificate signature validation failed' => $this->l10n->t('Certificate signature validation failed'), - // TRANSLATORS: Self-signed certificate is not present in trusted roots list. + // TRANSLATORS Self-signed certificate is not present in trusted roots list. 'Self-signed certificate not in trusted roots' => $this->l10n->t('Self-signed certificate not in trusted roots'), - // TRANSLATORS: Root certificate must be self-signed to be considered a trust anchor. + // TRANSLATORS Root certificate must be self-signed to be considered a trust anchor. 'Root certificate is not self-signed' => $this->l10n->t('Root certificate is not self-signed'), - // TRANSLATORS: Root certificate is not present in configured trusted certificate list. + // TRANSLATORS Root certificate is not present in configured trusted certificate list. 'Root certificate is not in trusted list' => $this->l10n->t('Root certificate is not in trusted list'), - // TRANSLATORS: Signature container has no binary signature payload. + // TRANSLATORS Signature container has no binary signature payload. 'No binary signature' => $this->l10n->t('No binary signature'), - // TRANSLATORS: Signature payload has no embedded certificates. + // TRANSLATORS Signature payload has no embedded certificates. 'No certificates in signature' => $this->l10n->t('No certificates in signature'), - // TRANSLATORS: Certificate used for signing is expired. + // TRANSLATORS Certificate used for signing is expired. 'Signing certificate has expired' => $this->l10n->t('Signing certificate has expired'), - // TRANSLATORS: Certificate used for signing is revoked. + // TRANSLATORS Certificate used for signing is revoked. 'Signing certificate has been revoked' => $this->l10n->t('Signing certificate has been revoked'), - // TRANSLATORS: Signature verification could not be fully completed. + // TRANSLATORS Signature verification could not be fully completed. 'Signature verification incomplete' => $this->l10n->t('Signature verification incomplete'), default => $reason, }; From 7bc2873af35dbdd84ca193e6f780263f0a35eb82 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:08:59 -0300 Subject: [PATCH 08/35] refactor: remove redundant docs from CA getters Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/Signature/PdfSignatureValidationService.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 93bf9cff87..efe4bd8f44 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -317,16 +317,10 @@ private function translateKnownReason(?string $reason): ?string { }; } - /** - * Check if LibreSign CA is loaded. - */ public function isLibreSignCaLoaded(): bool { return !empty($this->libresignCaCertificate); } - /** - * Get LibreSign CA certificate (PEM format). - */ public function getLibreSignCaCertificate(): string { return $this->libresignCaCertificate; } From f269442128e12f88bd806d76cce63ec5d30cafb5 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:09:45 -0300 Subject: [PATCH 09/35] refactor: remove redundant comments in validation service Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index efe4bd8f44..1cbe587885 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -37,11 +37,7 @@ public function __construct( $this->loadLibreSignCaCertificate(); } - /** - * Load LibreSign CA certificate and set it as trusted root. - */ private function loadLibreSignCaCertificate(): void { - // Try to load from config path first $configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path'); if (!empty($configPath) && is_dir($configPath)) { $caPemPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem'; @@ -55,7 +51,6 @@ private function loadLibreSignCaCertificate(): void { } } - // Try alternate location $alternateConfig = $this->appConfig->getValueString( Application::APP_ID, 'libresign_ca_certificate' @@ -66,18 +61,10 @@ private function loadLibreSignCaCertificate(): void { } } - /** - * Add additional trusted root certificate. - */ public function addTrustedRoot(string $certificatePem): void { $this->validator->addTrustedRoot($certificatePem); } - /** - * Set multiple trusted root certificates. - * - * @param list $certificates - */ public function setTrustedRoots(array $certificates): void { $this->validator->setTrustedRoots($certificates); } @@ -153,9 +140,6 @@ private function mapValidationResults(array $results, ?DateTime $signatureTime = return $mapped; } - /** - * Map signature validation result to LibreSign format. - */ private function mapSignatureValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::SIGNATURE_VALID => [ @@ -195,9 +179,6 @@ private function mapSignatureValidation(ValidationResult $result): array { }; } - /** - * Map certificate validation result to LibreSign format. - */ private function mapCertificateValidation(ValidationResult $result): array { return match ($result->state) { ValidationState::CERT_TRUSTED => [ From 835537120a722355390c7455506cff7ca187211a Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:12:59 -0300 Subject: [PATCH 10/35] test: remove deprecated reflection accessibility calls Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Service/Signature/PdfSignatureValidationServiceTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index cdc30d5a10..6101860832 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -81,7 +81,6 @@ private function newServiceWithoutConstructor(): PdfSignatureValidationService { $service = $reflection->newInstanceWithoutConstructor(); $l10nProperty = $reflection->getProperty('l10n'); - $l10nProperty->setAccessible(true); $l10nProperty->setValue($service, $this->l10n); return $service; @@ -93,7 +92,6 @@ private function newServiceWithoutConstructor(): PdfSignatureValidationService { private function invokePrivateMethod(PdfSignatureValidationService $service, string $method, ValidationResult $result): array { $reflection = new \ReflectionClass($service); $target = $reflection->getMethod($method); - $target->setAccessible(true); /** @var array $mapped */ $mapped = $target->invoke($service, $result); return $mapped; From f5e5a4a818660d34aee3743552ee0358ac1da0dc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:15:12 -0300 Subject: [PATCH 11/35] build: use released pdf-signature-validator v0.2.0 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 11 +------- composer.lock | 77 +++++++++++++++------------------------------------ 2 files changed, 23 insertions(+), 65 deletions(-) diff --git a/composer.json b/composer.json index c8cf094fed..538b7c8dde 100644 --- a/composer.json +++ b/composer.json @@ -1,13 +1,4 @@ { - "repositories": [ - { - "type": "path", - "url": "../pdf-signature-validator", - "options": { - "symlink": true - } - } - ], "require-dev": { "bamarni/composer-bin-plugin": "^1.8", "nextcloud/ocp": "dev-stable33", @@ -71,7 +62,7 @@ "require": { "cweagans/composer-patches": "^2.0", "jeidison/signer-php": "dev-main", - "libresign/pdf-signature-validator": "*@dev", + "libresign/pdf-signature-validator": "0.2.0", "phpseclib/phpseclib": "^3.0" } } diff --git a/composer.lock b/composer.lock index 0407a59725..513b740378 100644 --- a/composer.lock +++ b/composer.lock @@ -201,11 +201,17 @@ }, { "name": "libresign/pdf-signature-validator", - "version": "dev-backport/7525/stable33-manual", + "version": "v0.2.0", + "source": { + "type": "git", + "url": "https://github.com/LibreSign/pdf-signature-validator.git", + "reference": "f062333024dece3a8bb51da8bf8a6259e98543c2" + }, "dist": { - "type": "path", - "url": "../pdf-signature-validator", - "reference": "f8eba5ac5e43315f3856645e46e95eb21be4f742" + "type": "zip", + "url": "https://api.github.com/repos/LibreSign/pdf-signature-validator/zipball/f062333024dece3a8bb51da8bf8a6259e98543c2", + "reference": "f062333024dece3a8bb51da8bf8a6259e98543c2", + "shasum": "" }, "require": { "php": "^8.2", @@ -227,60 +233,22 @@ "LibreSign\\PdfSignatureValidator\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "LibreSign\\PdfSignatureValidator\\Tests\\": "tests/" - } - }, - "scripts": { - "bin": [ - "echo 'bin not installed'" - ], - "lint": [ - "find src tests -name '*.php' -print0 | xargs -0 -n1 php -l" - ], - "cs:check": [ - "php-cs-fixer fix --dry-run --diff" - ], - "cs:fix": [ - "php-cs-fixer fix" - ], - "phpmd": [ - "phpmd src,tests text phpmd.xml" - ], - "psalm": [ - "psalm --no-cache --threads=$(nproc)" - ], - "rector:check": [ - "rector --dry-run" - ], - "rector:fix": [ - "rector" - ], - "test:unit": [ - "phpunit --colors=always --fail-on-warning --fail-on-risky" - ], - "test:coverage": [ - "XDEBUG_MODE=coverage phpunit --coverage-text" - ], - "test:mutation": [ - "infection --threads=$(nproc) --skip-initial-tests" - ], - "post-install-cmd": [ - "@composer bin all install --ansi" - ], - "post-update-cmd": [ - "@composer bin all install --ansi" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "AGPL-3.0-or-later" ], "description": "High-quality PDF signature extraction and validation primitives for LibreSign and external consumers.", - "transport-options": { - "symlink": true, - "relative": true - } + "support": { + "issues": "https://github.com/LibreSign/pdf-signature-validator/issues", + "source": "https://github.com/LibreSign/pdf-signature-validator/tree/v0.2.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/libresign", + "type": "github" + } + ], + "time": "2026-04-24T00:11:10+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -1926,7 +1894,6 @@ "minimum-stability": "stable", "stability-flags": { "jeidison/signer-php": 20, - "libresign/pdf-signature-validator": 20, "nextcloud/ocp": 20, "roave/security-advisories": 20 }, From 135a11c3072ed81a45479656aa06a7c1a631d61d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:19:28 -0300 Subject: [PATCH 12/35] refactor: centralize validation localization mapping Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 72 +------------------ .../PdfSignatureValidationService.php | 42 +++++++++++ 2 files changed, 44 insertions(+), 70 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 1be6640f6e..2dfa331241 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -292,11 +292,11 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array } if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { - $leaf['signature_validation'] = $this->getLocalizedSignatureValidation($validation['signatureValidation']); + $leaf['signature_validation'] = $this->pdfSignatureValidationService->localizeSignatureValidation($validation['signatureValidation']); } if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { - $leaf['certificate_validation'] = $this->getLocalizedCertificateValidation($validation['certificateValidation']); + $leaf['certificate_validation'] = $this->pdfSignatureValidationService->localizeCertificateValidation($validation['certificateValidation']); } if (!isset($leaf['certificate_validation'])) { @@ -309,74 +309,6 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array return $result; } - /** - * Keep LibreSign-side l10n compatibility regardless of upstream validator labels. - */ - private function getLocalizedSignatureValidation(array $validation): array { - $id = (int)($validation['id'] ?? 6); - - return match ($id) { - 1 => [ - 'id' => 1, - 'label' => $this->l10n->t('Signature is valid.'), - ], - 2 => [ - 'id' => 2, - 'label' => $this->l10n->t('Signature is invalid.'), - ], - 3 => [ - 'id' => 3, - 'label' => $this->l10n->t('Digest mismatch.'), - ], - 5 => [ - 'id' => 5, - 'label' => $this->l10n->t('Signature has not yet been verified.'), - ], - default => [ - 'id' => 6, - 'label' => $this->l10n->t('Unknown validation failure.'), - ], - }; - } - - /** - * Keep LibreSign-side l10n compatibility regardless of upstream validator labels. - */ - private function getLocalizedCertificateValidation(array $validation): array { - $id = (int)($validation['id'] ?? 7); - - return match ($id) { - 1 => [ - 'id' => 1, - 'label' => $this->l10n->t('Certificate is trusted.'), - ], - 2 => [ - 'id' => 2, - 'label' => $this->l10n->t("Certificate issuer isn't trusted."), - ], - 3 => [ - 'id' => 3, - 'label' => $this->l10n->t('Certificate issuer is unknown.'), - ], - 4 => [ - 'id' => 4, - 'label' => $this->l10n->t('Certificate has been revoked.'), - ], - 5 => [ - 'id' => 5, - 'label' => $this->l10n->t('Certificate has expired'), - ], - 6 => [ - 'id' => 6, - 'label' => $this->l10n->t('Certificate has not yet been verified.'), - ], - default => [ - 'id' => 7, - 'label' => $this->l10n->t('Unknown issue with certificate or corrupted data.'), - ], - }; - } - /** * @param resource $resource * @return list diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 1cbe587885..74a4ab8c17 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -69,6 +69,48 @@ public function setTrustedRoots(array $certificates): void { $this->validator->setTrustedRoots($certificates); } + /** + * Normalize a signature validation payload by id/reason to the canonical LibreSign shape. + * + * @param array{id?: int|string, reason?: mixed} $validation + */ + public function localizeSignatureValidation(array $validation): array { + $id = (int)($validation['id'] ?? 6); + $reason = is_string($validation['reason'] ?? null) ? $validation['reason'] : null; + + $state = match ($id) { + 1 => ValidationState::SIGNATURE_VALID, + 2 => ValidationState::SIGNATURE_INVALID, + 3 => ValidationState::DIGEST_MISMATCH, + 5 => ValidationState::NOT_VERIFIED, + default => ValidationState::UNKNOWN_FAILURE, + }; + + return $this->mapSignatureValidation(new ValidationResult($state, $reason)); + } + + /** + * Normalize a certificate validation payload by id/reason to the canonical LibreSign shape. + * + * @param array{id?: int|string, reason?: mixed} $validation + */ + public function localizeCertificateValidation(array $validation): array { + $id = (int)($validation['id'] ?? 7); + $reason = is_string($validation['reason'] ?? null) ? $validation['reason'] : null; + + $state = match ($id) { + 1 => ValidationState::CERT_TRUSTED, + 2 => ValidationState::CERT_ISSUER_NOT_TRUSTED, + 3 => ValidationState::CERT_ISSUER_UNKNOWN, + 4 => ValidationState::CERT_REVOKED, + 5 => ValidationState::CERT_EXPIRED, + 6 => ValidationState::CERT_NOT_VERIFIED, + default => ValidationState::UNKNOWN_FAILURE, + }; + + return $this->mapCertificateValidation(new ValidationResult($state, $reason)); + } + /** * Validate PDF signatures from file resource. * From 0c1a65d6f30ceb8b43646a3b2c6da646465a734b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:21:15 -0300 Subject: [PATCH 13/35] fix: remove invalid UNKNOWN_FAILURE enum state Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/Signature/PdfSignatureValidationService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 74a4ab8c17..a929c56f03 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -83,7 +83,7 @@ public function localizeSignatureValidation(array $validation): array { 2 => ValidationState::SIGNATURE_INVALID, 3 => ValidationState::DIGEST_MISMATCH, 5 => ValidationState::NOT_VERIFIED, - default => ValidationState::UNKNOWN_FAILURE, + default => ValidationState::NOT_VERIFIED, }; return $this->mapSignatureValidation(new ValidationResult($state, $reason)); @@ -105,7 +105,7 @@ public function localizeCertificateValidation(array $validation): array { 4 => ValidationState::CERT_REVOKED, 5 => ValidationState::CERT_EXPIRED, 6 => ValidationState::CERT_NOT_VERIFIED, - default => ValidationState::UNKNOWN_FAILURE, + default => ValidationState::CERT_NOT_VERIFIED, }; return $this->mapCertificateValidation(new ValidationResult($state, $reason)); From e7e86a39c63b208f87c8b21ef97373e68905ac28 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:24:44 -0300 Subject: [PATCH 14/35] refactor: remove redundant localize* methods The service's validateFromResource() already returns properly formatted arrays with id, label, isValid, and reason. The localize* methods were unnecessarily converting numbers back to enums and then back to numbers. Now Pkcs12Handler uses the validation results directly without the circular conversion, eliminating code duplication and improving clarity. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 4 +- .../PdfSignatureValidationService.php | 42 ------------------- 2 files changed, 2 insertions(+), 44 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 2dfa331241..bed0602c00 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -292,11 +292,11 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array } if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { - $leaf['signature_validation'] = $this->pdfSignatureValidationService->localizeSignatureValidation($validation['signatureValidation']); + $leaf['signature_validation'] = $validation['signatureValidation']; } if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { - $leaf['certificate_validation'] = $this->pdfSignatureValidationService->localizeCertificateValidation($validation['certificateValidation']); + $leaf['certificate_validation'] = $validation['certificateValidation']; } if (!isset($leaf['certificate_validation'])) { diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index a929c56f03..1cbe587885 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -69,48 +69,6 @@ public function setTrustedRoots(array $certificates): void { $this->validator->setTrustedRoots($certificates); } - /** - * Normalize a signature validation payload by id/reason to the canonical LibreSign shape. - * - * @param array{id?: int|string, reason?: mixed} $validation - */ - public function localizeSignatureValidation(array $validation): array { - $id = (int)($validation['id'] ?? 6); - $reason = is_string($validation['reason'] ?? null) ? $validation['reason'] : null; - - $state = match ($id) { - 1 => ValidationState::SIGNATURE_VALID, - 2 => ValidationState::SIGNATURE_INVALID, - 3 => ValidationState::DIGEST_MISMATCH, - 5 => ValidationState::NOT_VERIFIED, - default => ValidationState::NOT_VERIFIED, - }; - - return $this->mapSignatureValidation(new ValidationResult($state, $reason)); - } - - /** - * Normalize a certificate validation payload by id/reason to the canonical LibreSign shape. - * - * @param array{id?: int|string, reason?: mixed} $validation - */ - public function localizeCertificateValidation(array $validation): array { - $id = (int)($validation['id'] ?? 7); - $reason = is_string($validation['reason'] ?? null) ? $validation['reason'] : null; - - $state = match ($id) { - 1 => ValidationState::CERT_TRUSTED, - 2 => ValidationState::CERT_ISSUER_NOT_TRUSTED, - 3 => ValidationState::CERT_ISSUER_UNKNOWN, - 4 => ValidationState::CERT_REVOKED, - 5 => ValidationState::CERT_EXPIRED, - 6 => ValidationState::CERT_NOT_VERIFIED, - default => ValidationState::CERT_NOT_VERIFIED, - }; - - return $this->mapCertificateValidation(new ValidationResult($state, $reason)); - } - /** * Validate PDF signatures from file resource. * From 9ceba01e1ee51f5ef7e9f5f8a9024e46e745c624 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:26:34 -0300 Subject: [PATCH 15/35] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/Signature/PdfSignatureValidationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 1cbe587885..4bc9550391 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -9,9 +9,9 @@ namespace OCA\Libresign\Service\Signature; use DateTime; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; use LibreSign\PdfSignatureValidator\Model\ValidationResult; use LibreSign\PdfSignatureValidator\Model\ValidationState; +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; use OCA\Libresign\AppInfo\Application; use OCP\IAppConfig; use OCP\IL10N; From 79f25fca02d3d79ae09f863635561cd3e3d89d3f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:30:24 -0300 Subject: [PATCH 16/35] fix: resolve Psalm type hint errors - FileController::fetchPreview: add inline type assertions for RedirectResponse to specify exact status code (303) when returning null-checked values - PdfSignatureValidationService: update docblocks for validateFromResource, validateFromString, and mapValidationResults to include the 'raw' field - Pkcs12Handler::extractNativeSignatureMetadata: change return type from list to array to match actual implementation All Psalm errors resolved, unit tests still passing. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Controller/FileController.php | 45 ++++++++++++++++--- lib/Handler/SignEngine/Pkcs12Handler.php | 2 +- .../PdfSignatureValidationService.php | 6 +-- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/lib/Controller/FileController.php b/lib/Controller/FileController.php index bac8b005a1..c30d0869df 100644 --- a/lib/Controller/FileController.php +++ b/lib/Controller/FileController.php @@ -457,7 +457,7 @@ private function fetchPreview( bool $forceIcon, string $mode, bool $mimeFallback = false, - ) : Http\Response { + ): Http\Response { if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) { return new DataResponse([], Http::STATUS_NOT_FOUND); } @@ -465,6 +465,15 @@ private function fetchPreview( return new DataResponse([], Http::STATUS_FORBIDDEN); } + // Avoid expensive external preview generators for PDFs when a mime fallback is explicitly requested. + if ($mimeFallback && $node->getMimeType() === 'application/pdf') { + $mimeFallbackResponse = $this->getMimeFallbackResponse($node->getMimeType()); + if ($mimeFallbackResponse !== null) { + /** @var Http\RedirectResponse $mimeFallbackResponse */ + return $mimeFallbackResponse; + } + } + $storage = $node->getStorage(); if ($storage->instanceOfStorage(SharedStorage::class)) { /** @var SharedStorage $storage */ @@ -483,19 +492,43 @@ private function fetchPreview( $response->cacheFor(3600 * 24, false, true); return $response; } catch (NotFoundException) { - // If we have no preview enabled, we can redirect to the mime icon if any - if ($mimeFallback) { - if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) { - return new RedirectResponse($url); - } + $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null; + if ($mimeFallbackResponse !== null) { + /** @var Http\RedirectResponse $mimeFallbackResponse */ + return $mimeFallbackResponse; } return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException) { return new DataResponse([], Http::STATUS_BAD_REQUEST); + } catch (\Throwable $e) { + $this->logger->warning('Failed to generate LibreSign thumbnail preview', [ + 'nodeId' => $node->getId(), + 'mimeType' => $node->getMimeType(), + 'exception' => $e, + ]); + + $mimeFallbackResponse = $mimeFallback ? $this->getMimeFallbackResponse($node->getMimeType()) : null; + if ($mimeFallbackResponse !== null) { + /** @var Http\RedirectResponse $mimeFallbackResponse */ + return $mimeFallbackResponse; + } + + return new DataResponse([], Http::STATUS_NOT_FOUND); } } + private function getMimeFallbackResponse(string $mimeType): ?\OCP\AppFramework\Http\RedirectResponse { + $url = $this->mimeIconProvider->getMimeIconUrl($mimeType); + if ($url) { + /** @var \OCP\AppFramework\Http\RedirectResponse $response */ + $response = new RedirectResponse($url, Http::STATUS_SEE_OTHER); + return $response; + } + + return null; + } + /** * Send a file * diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index bed0602c00..7093d197b5 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -311,7 +311,7 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array /** * @param resource $resource - * @return list + * @return array */ private function extractNativeSignatureMetadata($resource): array { rewind($resource); diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 4bc9550391..8e13f3b07a 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -74,7 +74,7 @@ public function setTrustedRoots(array $certificates): void { * * @param resource $resource PDF file resource * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) - * @return list + * @return list */ public function validateFromResource($resource, ?DateTime $signatureTime = null): array { try { @@ -94,7 +94,7 @@ public function validateFromResource($resource, ?DateTime $signatureTime = null) * * @param string $pdfContent Binary PDF content * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) - * @return list + * @return list */ public function validateFromString(string $pdfContent, ?DateTime $signatureTime = null): array { try { @@ -114,7 +114,7 @@ public function validateFromString(string $pdfContent, ?DateTime $signatureTime * * @param list $results Results from PdfSignatureValidator * @param ?\DateTime $signatureTime - * @return list + * @return list */ private function mapValidationResults(array $results, ?DateTime $signatureTime = null): array { $mapped = []; From ff5261d4b4ddf22dfff62847318ed84ff9f70ec1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:36:45 -0300 Subject: [PATCH 17/35] fix: update PasswordTest constructor arguments for Pkcs12Handler Update constructor arguments to match the new signature which includes PdfSignatureValidationService and PdfSignatureExtractor, and removes the no-longer-used TempManager. Also use real instance of PdfSignatureExtractor instead of mock since the class is final and cannot be mocked. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../php/Unit/Service/IdentifyMethod/PasswordTest.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index f335561a32..8de2878645 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -20,9 +20,10 @@ use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\IdentifyMethod\IdentifyService; use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\Password; +use OCA\Libresign\Service\Signature\PdfSignatureValidationService; +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCP\IAppConfig; use OCP\IL10N; -use OCP\ITempManager; use OCP\IUserSession; use OCP\L10N\IFactory as IL10NFactory; use PHPUnit\Framework\Attributes\DataProvider; @@ -38,11 +39,12 @@ final class PasswordTest extends \OCA\Libresign\Tests\Unit\TestCase { private CertificateEngineFactory&MockObject $certificateEngineFactory; private IL10N $l10n; private FooterHandler&MockObject $footerHandler; - private ITempManager $tempManager; private LoggerInterface&MockObject $logger; private CaIdentifierService&MockObject $caIdentifierService; private DocMdpHandler&MockObject $docMdpHandler; private CrlService&MockObject $crlService; + private PdfSignatureValidationService&MockObject $pdfSignatureValidationService; + private PdfSignatureExtractor $pdfSignatureExtractor; public function setUp(): void { $this->identifyService = $this->createMock(IdentifyService::class); @@ -51,12 +53,13 @@ public function setUp(): void { $this->certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); $this->l10n = \OCP\Server::get(IL10NFactory::class)->get(Application::APP_ID); $this->footerHandler = $this->createMock(FooterHandler::class); - $this->tempManager = \OCP\Server::get(ITempManager::class); $this->userSession = $this->createMock(IUserSession::class); $this->logger = $this->createMock(LoggerInterface::class); $this->caIdentifierService = $this->createMock(CaIdentifierService::class); $this->docMdpHandler = $this->createMock(DocMdpHandler::class); $this->crlService = $this->createMock(CrlService::class); + $this->pdfSignatureValidationService = $this->createMock(PdfSignatureValidationService::class); + $this->pdfSignatureExtractor = new PdfSignatureExtractor(); $this->pkcs12Handler = $this->getPkcs12Instance(); } @@ -79,11 +82,12 @@ private function getPkcs12Instance(array $methods = []) { $this->certificateEngineFactory, $this->l10n, $this->footerHandler, - $this->tempManager, $this->logger, $this->caIdentifierService, $this->docMdpHandler, $this->crlService, + $this->pdfSignatureValidationService, + $this->pdfSignatureExtractor, ]) ->onlyMethods($methods) ->getMock(); From f65fc6c9b249d43cd9bfdfc0fd2739909a5038ca Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:38:12 -0300 Subject: [PATCH 18/35] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/IdentifyMethod/PasswordTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index 8de2878645..4cd30cc43c 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -8,6 +8,7 @@ namespace OCA\Libresign\Tests\Unit\Service\IdentifyMethod; +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Enum\CrlValidationStatus; use OCA\Libresign\Exception\LibresignException; @@ -21,7 +22,6 @@ use OCA\Libresign\Service\IdentifyMethod\IdentifyService; use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\Password; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCP\IAppConfig; use OCP\IL10N; use OCP\IUserSession; From 1b3a5d3bb80b88f0f833b14d15a5f44a421d2872 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:08:43 -0300 Subject: [PATCH 19/35] fix: preserve legacy signature validation on native digest mismatch Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 16 ++++++++++++- .../Handler/SignEngine/Pkcs12HandlerTest.php | 24 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 7093d197b5..e95121471a 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -292,7 +292,12 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array } if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { - $leaf['signature_validation'] = $validation['signatureValidation']; + $signatureValidation = $validation['signatureValidation']; + + // Keep legacy OpenSSL result when native validator reports this known false-positive. + if (!$this->isDigestMismatchSignatureValidation($signatureValidation)) { + $leaf['signature_validation'] = $signatureValidation; + } } if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { @@ -309,6 +314,15 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array return $result; } + /** + * signer engines can produce signatures that the native validator currently flags as digest mismatch. + * In this case we preserve the legacy validation computed from the PKCS#7 signature. + */ + private function isDigestMismatchSignatureValidation(array $signatureValidation): bool { + return ($signatureValidation['id'] ?? null) === 3 + && ($signatureValidation['label'] ?? '') === 'Digest mismatch.'; + } + /** * @param resource $resource * @return array diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 11a5f15b20..91d86128d2 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -487,4 +487,28 @@ public function testGetCertificateChainUsesNativeValidationServiceForEachSignatu $this->assertSame(3, $result[0]['chain'][0]['certificate_validation']['id']); } + public function testGetCertificateChainDoesNotOverrideLegacySignatureValidationOnDigestMismatch(): void { + $this->pdfSignatureValidationService->method('validateFromResource') + ->willReturn([ + [ + 'signatureValidation' => [ + 'id' => 3, + 'label' => 'Digest mismatch.', + 'reason' => 'PDF content hash does not match signed digest', + ], + ], + ]); + + $handler = $this->getHandler(); + $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); + $this->assertIsResource($resource); + + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertNotEmpty($result); + $this->assertSame(1, $result[0]['chain'][0]['signature_validation']['id']); + $this->assertSame('Signature is valid.', $result[0]['chain'][0]['signature_validation']['label']); + } + } From 0121ba4d3323f0af49cae9a887bb5cc8d02ff2ab Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:36:13 -0300 Subject: [PATCH 20/35] fix: address native validator review feedback Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 2 +- composer.lock | 12 ++++++------ lib/Handler/SignEngine/Pkcs12Handler.php | 17 +++++++++++++---- .../Signature/PdfSignatureValidationService.php | 14 +++++--------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/composer.json b/composer.json index 538b7c8dde..9b87491972 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "require": { "cweagans/composer-patches": "^2.0", "jeidison/signer-php": "dev-main", - "libresign/pdf-signature-validator": "0.2.0", + "libresign/pdf-signature-validator": "^0.2.0", "phpseclib/phpseclib": "^3.0" } } diff --git a/composer.lock b/composer.lock index 513b740378..09f76222d3 100644 --- a/composer.lock +++ b/composer.lock @@ -201,16 +201,16 @@ }, { "name": "libresign/pdf-signature-validator", - "version": "v0.2.0", + "version": "v0.2.1", "source": { "type": "git", "url": "https://github.com/LibreSign/pdf-signature-validator.git", - "reference": "f062333024dece3a8bb51da8bf8a6259e98543c2" + "reference": "8288a4648c8d738fe538f182bb8a9c6a123feb1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LibreSign/pdf-signature-validator/zipball/f062333024dece3a8bb51da8bf8a6259e98543c2", - "reference": "f062333024dece3a8bb51da8bf8a6259e98543c2", + "url": "https://api.github.com/repos/LibreSign/pdf-signature-validator/zipball/8288a4648c8d738fe538f182bb8a9c6a123feb1a", + "reference": "8288a4648c8d738fe538f182bb8a9c6a123feb1a", "shasum": "" }, "require": { @@ -240,7 +240,7 @@ "description": "High-quality PDF signature extraction and validation primitives for LibreSign and external consumers.", "support": { "issues": "https://github.com/LibreSign/pdf-signature-validator/issues", - "source": "https://github.com/LibreSign/pdf-signature-validator/tree/v0.2.0" + "source": "https://github.com/LibreSign/pdf-signature-validator/tree/v0.2.1" }, "funding": [ { @@ -248,7 +248,7 @@ "type": "github" } ], - "time": "2026-04-24T00:11:10+00:00" + "time": "2026-04-24T14:34:32+00:00" }, { "name": "paragonie/constant_time_encoding", diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index e95121471a..5b1bdf7f2b 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -9,6 +9,9 @@ namespace OCA\Libresign\Handler\SignEngine; use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; +use LibreSign\PdfSignatureValidator\Model\ValidationReason; +use LibreSign\PdfSignatureValidator\Model\ValidationResult; +use LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; @@ -295,7 +298,7 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array $signatureValidation = $validation['signatureValidation']; // Keep legacy OpenSSL result when native validator reports this known false-positive. - if (!$this->isDigestMismatchSignatureValidation($signatureValidation)) { + if (!$this->isDigestMismatchSignatureValidation($validation)) { $leaf['signature_validation'] = $signatureValidation; } } @@ -318,9 +321,15 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array * signer engines can produce signatures that the native validator currently flags as digest mismatch. * In this case we preserve the legacy validation computed from the PKCS#7 signature. */ - private function isDigestMismatchSignatureValidation(array $signatureValidation): bool { - return ($signatureValidation['id'] ?? null) === 3 - && ($signatureValidation['label'] ?? '') === 'Digest mismatch.'; + private function isDigestMismatchSignatureValidation(array $validation): bool { + $rawSignatureValidation = $validation['raw']['signature'] ?? null; + if ($rawSignatureValidation instanceof ValidationResult) { + return $rawSignatureValidation->reasonCode === ValidationReason::DIGEST_MISMATCH + || $rawSignatureValidation->state === ValidationState::DIGEST_MISMATCH; + } + + $signatureValidation = $validation['signatureValidation'] ?? null; + return is_array($signatureValidation) && ($signatureValidation['id'] ?? null) === 3; } /** diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 8e13f3b07a..ddabf3c65c 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -8,7 +8,6 @@ namespace OCA\Libresign\Service\Signature; -use DateTime; use LibreSign\PdfSignatureValidator\Model\ValidationResult; use LibreSign\PdfSignatureValidator\Model\ValidationState; use LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; @@ -73,13 +72,12 @@ public function setTrustedRoots(array $certificates): void { * Validate PDF signatures from file resource. * * @param resource $resource PDF file resource - * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) * @return list */ - public function validateFromResource($resource, ?DateTime $signatureTime = null): array { + public function validateFromResource($resource): array { try { $results = $this->validator->validateFromResource($resource); - return $this->mapValidationResults($results, $signatureTime); + return $this->mapValidationResults($results); } catch (\Throwable $e) { $this->logger->warning('PDF signature validation failed', [ 'error' => $e->getMessage(), @@ -93,13 +91,12 @@ public function validateFromResource($resource, ?DateTime $signatureTime = null) * Validate PDF signatures from binary content. * * @param string $pdfContent Binary PDF content - * @param ?\DateTime $signatureTime Optional time to validate against (for historic validation) * @return list */ - public function validateFromString(string $pdfContent, ?DateTime $signatureTime = null): array { + public function validateFromString(string $pdfContent): array { try { $results = $this->validator->validateFromString($pdfContent); - return $this->mapValidationResults($results, $signatureTime); + return $this->mapValidationResults($results); } catch (\Throwable $e) { $this->logger->warning('PDF signature validation failed', [ 'error' => $e->getMessage(), @@ -113,10 +110,9 @@ public function validateFromString(string $pdfContent, ?DateTime $signatureTime * Map validation results from PdfSignatureValidator to LibreSign format. * * @param list $results Results from PdfSignatureValidator - * @param ?\DateTime $signatureTime * @return list */ - private function mapValidationResults(array $results, ?DateTime $signatureTime = null): array { + private function mapValidationResults(array $results): array { $mapped = []; foreach ($results as $result) { From 72b71268babb8b64582e6017e3b7a9be128cd666 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:40:27 -0300 Subject: [PATCH 21/35] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 5b1bdf7f2b..68b39515ff 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -8,10 +8,10 @@ namespace OCA\Libresign\Handler\SignEngine; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use LibreSign\PdfSignatureValidator\Model\ValidationReason; use LibreSign\PdfSignatureValidator\Model\ValidationResult; use LibreSign\PdfSignatureValidator\Model\ValidationState; +use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; From 72e0c8f7873adaf744a5ed1f8f74b7b4336ffccf Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:46:57 -0300 Subject: [PATCH 22/35] fix: remove phpseclib from direct deps, use provide via 3rdparty phpseclib is already vendored and scoped in the 3rdparty submodule under OCA\Libresign\Vendor\phpseclib3. Declaring it as a direct composer dependency caused roave/security-advisories to block the insecure 3.0.52 version locked in composer.lock. Remove phpseclib/phpseclib, paragonie/constant_time_encoding and paragonie/random_compat from composer.json require and composer.lock. Use "provide" to satisfy the transitive dependency declared by libresign/pdf-signature-validator without installing the package. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 4 +- composer.lock | 229 -------------------------------------------------- 2 files changed, 3 insertions(+), 230 deletions(-) diff --git a/composer.json b/composer.json index 9b87491972..3826156bcf 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,9 @@ "require": { "cweagans/composer-patches": "^2.0", "jeidison/signer-php": "dev-main", - "libresign/pdf-signature-validator": "^0.2.0", + "libresign/pdf-signature-validator": "^0.2.0" + }, + "provide": { "phpseclib/phpseclib": "^3.0" } } diff --git a/composer.lock b/composer.lock index 09f76222d3..7a2ed5ccde 100644 --- a/composer.lock +++ b/composer.lock @@ -249,235 +249,6 @@ } ], "time": "2026-04-24T14:34:32+00:00" - }, - { - "name": "paragonie/constant_time_encoding", - "version": "v3.1.3", - "source": { - "type": "git", - "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "shasum": "" - }, - "require": { - "php": "^8" - }, - "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "ParagonIE\\ConstantTime\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com", - "role": "Maintainer" - }, - { - "name": "Steve 'Sc00bz' Thomas", - "email": "steve@tobtu.com", - "homepage": "https://www.tobtu.com", - "role": "Original Developer" - } - ], - "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", - "keywords": [ - "base16", - "base32", - "base32_decode", - "base32_encode", - "base64", - "base64_decode", - "base64_encode", - "bin2hex", - "encoding", - "hex", - "hex2bin", - "rfc4648" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/constant_time_encoding/issues", - "source": "https://github.com/paragonie/constant_time_encoding" - }, - "time": "2025-09-24T15:06:41+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, - { - "name": "phpseclib/phpseclib", - "version": "3.0.52", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", - "shasum": "" - }, - "require": { - "paragonie/constant_time_encoding": "^1|^2|^3", - "paragonie/random_compat": "^1.4|^2.0|^9.99.99", - "php": ">=5.6.1" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "suggest": { - "ext-dom": "Install the DOM extension to load XML formatted public keys.", - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." - }, - "type": "library", - "autoload": { - "files": [ - "phpseclib/bootstrap.php" - ], - "psr-4": { - "phpseclib3\\": "phpseclib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "support": { - "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" - }, - "funding": [ - { - "url": "https://github.com/terrafrost", - "type": "github" - }, - { - "url": "https://www.patreon.com/phpseclib", - "type": "patreon" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", - "type": "tidelift" - } - ], - "time": "2026-04-27T07:02:15+00:00" } ], "packages-dev": [ From b46a2f2cb77be81bace285994b66c77637d35d4e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:13:55 -0300 Subject: [PATCH 23/35] refactor: move pdf-signature-validator to 3rdparty scoped vendor The libresign/pdf-signature-validator package is moved from libresign's own composer dependencies into the 3rdparty scoped vendor directory so that php-scoper can prefix both its classes and its phpseclib3 references with OCA\Libresign\Vendor\. - Remove libresign/pdf-signature-validator from composer.json + lock - Remove phpseclib/phpseclib 'provide' declaration (no longer needed) - Update all use statements: LibreSign\PdfSignatureValidator\... => OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\... - Update 3rdparty submodule ref to include the scoped package Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- composer.json | 6 +-- composer.lock | 51 ------------------- lib/Handler/SignEngine/Pkcs12Handler.php | 8 +-- .../PdfSignatureValidationService.php | 6 +-- .../Handler/SignEngine/Pkcs12HandlerTest.php | 2 +- .../Service/IdentifyMethod/PasswordTest.php | 2 +- .../PdfSignatureValidationServiceTest.php | 4 +- 7 files changed, 12 insertions(+), 67 deletions(-) diff --git a/composer.json b/composer.json index 3826156bcf..a0e68d0fda 100644 --- a/composer.json +++ b/composer.json @@ -61,10 +61,6 @@ }, "require": { "cweagans/composer-patches": "^2.0", - "jeidison/signer-php": "dev-main", - "libresign/pdf-signature-validator": "^0.2.0" - }, - "provide": { - "phpseclib/phpseclib": "^3.0" + "jeidison/signer-php": "dev-main" } } diff --git a/composer.lock b/composer.lock index 7a2ed5ccde..5e60c2a576 100644 --- a/composer.lock +++ b/composer.lock @@ -198,57 +198,6 @@ "source": "https://github.com/jeidison/signer-php/tree/main" }, "time": "2026-05-27T18:35:28+00:00" - }, - { - "name": "libresign/pdf-signature-validator", - "version": "v0.2.1", - "source": { - "type": "git", - "url": "https://github.com/LibreSign/pdf-signature-validator.git", - "reference": "8288a4648c8d738fe538f182bb8a9c6a123feb1a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/LibreSign/pdf-signature-validator/zipball/8288a4648c8d738fe538f182bb8a9c6a123feb1a", - "reference": "8288a4648c8d738fe538f182bb8a9c6a123feb1a", - "shasum": "" - }, - "require": { - "php": "^8.2", - "phpseclib/phpseclib": "^3.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8", - "roave/security-advisories": "dev-latest" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": true - } - }, - "autoload": { - "psr-4": { - "LibreSign\\PdfSignatureValidator\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "AGPL-3.0-or-later" - ], - "description": "High-quality PDF signature extraction and validation primitives for LibreSign and external consumers.", - "support": { - "issues": "https://github.com/LibreSign/pdf-signature-validator/issues", - "source": "https://github.com/LibreSign/pdf-signature-validator/tree/v0.2.1" - }, - "funding": [ - { - "url": "https://github.com/sponsors/libresign", - "type": "github" - } - ], - "time": "2026-04-24T14:34:32+00:00" } ], "packages-dev": [ diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 68b39515ff..5ad20592f7 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -8,10 +8,10 @@ namespace OCA\Libresign\Handler\SignEngine; -use LibreSign\PdfSignatureValidator\Model\ValidationReason; -use LibreSign\PdfSignatureValidator\Model\ValidationResult; -use LibreSign\PdfSignatureValidator\Model\ValidationState; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index ddabf3c65c..60acdbaec3 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -8,9 +8,9 @@ namespace OCA\Libresign\Service\Signature; -use LibreSign\PdfSignatureValidator\Model\ValidationResult; -use LibreSign\PdfSignatureValidator\Model\ValidationState; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; use OCA\Libresign\AppInfo\Application; use OCP\IAppConfig; use OCP\IL10N; diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 91d86128d2..e0aac62977 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -9,7 +9,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\DocMdpHandler; diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index 4cd30cc43c..02ddba03e8 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -8,7 +8,7 @@ namespace OCA\Libresign\Tests\Unit\Service\IdentifyMethod; -use LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Enum\CrlValidationStatus; use OCA\Libresign\Exception\LibresignException; diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index 6101860832..ac4294dade 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -8,8 +8,8 @@ namespace OCA\Libresign\Tests\Unit\Service\Signature; -use LibreSign\PdfSignatureValidator\Model\ValidationResult; -use LibreSign\PdfSignatureValidator\Model\ValidationState; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Unit\TestCase; use OCP\IL10N; From acaccfd51488343c1a81dad5399bc96c3d53d95c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:24:15 -0300 Subject: [PATCH 24/35] fix(cs): fix use statement ordering per php-cs-fixer Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 8 ++++---- lib/Service/Signature/PdfSignatureValidationService.php | 2 +- tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php | 2 +- tests/php/Unit/Service/IdentifyMethod/PasswordTest.php | 2 +- .../Signature/PdfSignatureValidationServiceTest.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 5ad20592f7..881ae331fd 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -8,10 +8,6 @@ namespace OCA\Libresign\Handler\SignEngine; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; @@ -22,6 +18,10 @@ use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\Vendor\phpseclib3\File\ASN1; use OCP\Files\File; use OCP\IAppConfig; diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 60acdbaec3..817c75eeb8 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -8,10 +8,10 @@ namespace OCA\Libresign\Service\Signature; +use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; -use OCA\Libresign\AppInfo\Application; use OCP\IAppConfig; use OCP\IL10N; use Psr\Log\LoggerInterface; diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index e0aac62977..1d804a0424 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -9,7 +9,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\DocMdpHandler; @@ -20,6 +19,7 @@ use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAppConfig; diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index 02ddba03e8..b205928516 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -8,7 +8,6 @@ namespace OCA\Libresign\Tests\Unit\Service\IdentifyMethod; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Enum\CrlValidationStatus; use OCA\Libresign\Exception\LibresignException; @@ -22,6 +21,7 @@ use OCA\Libresign\Service\IdentifyMethod\IdentifyService; use OCA\Libresign\Service\IdentifyMethod\SignatureMethod\Password; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; use OCP\IAppConfig; use OCP\IL10N; use OCP\IUserSession; diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index ac4294dade..1380eca9b2 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -8,10 +8,10 @@ namespace OCA\Libresign\Tests\Unit\Service\Signature; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; -use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Unit\TestCase; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCP\IL10N; use PHPUnit\Framework\MockObject\MockObject; From 7cc2f0dc9c9d459a42a35c21ddad0edcdb38bc77 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:43:14 -0300 Subject: [PATCH 25/35] fix: propagate unexpected native validation exceptions Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 817c75eeb8..2a222deb4c 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -9,6 +9,7 @@ namespace OCA\Libresign\Service\Signature; use OCA\Libresign\AppInfo\Application; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureValidator; @@ -76,15 +77,12 @@ public function setTrustedRoots(array $certificates): void { */ public function validateFromResource($resource): array { try { - $results = $this->validator->validateFromResource($resource); - return $this->mapValidationResults($results); - } catch (\Throwable $e) { - $this->logger->warning('PDF signature validation failed', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); + $results = $this->validateNativeFromResource($resource); + } catch (UnsignedPdfException) { return []; } + + return $this->mapValidationResults($results); } /** @@ -95,15 +93,27 @@ public function validateFromResource($resource): array { */ public function validateFromString(string $pdfContent): array { try { - $results = $this->validator->validateFromString($pdfContent); - return $this->mapValidationResults($results); - } catch (\Throwable $e) { - $this->logger->warning('PDF signature validation failed', [ - 'error' => $e->getMessage(), - 'trace' => $e->getTraceAsString(), - ]); + $results = $this->validateNativeFromString($pdfContent); + } catch (UnsignedPdfException) { return []; } + + return $this->mapValidationResults($results); + } + + /** + * @param resource $resource + * @return list, certificateValidation: ValidationResult}> + */ + protected function validateNativeFromResource($resource): array { + return $this->validator->validateFromResource($resource); + } + + /** + * @return list, certificateValidation: ValidationResult}> + */ + protected function validateNativeFromString(string $pdfContent): array { + return $this->validator->validateFromString($pdfContent); } /** From 84d7a69eb20fb3d02c2250d726a0632eeff49c58 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:43:14 -0300 Subject: [PATCH 26/35] fix: always reset policy context on metadata failures Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 881ae331fd..031e84e0a6 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors - * SPDX-License-Identifier: AGPL-3.0-or-later + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Libresign\Handler\SignEngine; @@ -18,6 +18,7 @@ use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; @@ -344,8 +345,8 @@ private function extractNativeSignatureMetadata($resource): array { } try { - $signatures = $this->pdfSignatureExtractor->extractFromString($content); - } catch (\Throwable) { + $signatures = $this->extractNativeSignaturesFromContent($content); + } catch (UnsignedPdfException) { return []; } $metadata = []; @@ -362,6 +363,10 @@ private function extractNativeSignatureMetadata($resource): array { return $metadata; } + protected function extractNativeSignaturesFromContent(string $content): array { + return $this->pdfSignatureExtractor->extractFromString($content); + } + private function der2pem($derData) { $pem = chunk_split(base64_encode((string)$derData), 64, "\n"); $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; From 7cb6ab14fec5f43e52acc950a45dd5cea1985a22 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:43:14 -0300 Subject: [PATCH 27/35] test: cover native validation exception flow Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationServiceTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index 1380eca9b2..137e1e1e9e 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -10,20 +10,28 @@ use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Unit\TestCase; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; +use OCP\IAppConfig; use OCP\IL10N; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; final class PdfSignatureValidationServiceTest extends TestCase { + private IAppConfig&MockObject $appConfig; private IL10N&MockObject $l10n; + private LoggerInterface&MockObject $logger; public function setUp(): void { parent::setUp(); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->appConfig->method('getValueString')->willReturn(''); $this->l10n = $this->createMock(IL10N::class); $this->l10n ->method('t') ->willReturnCallback(static fn (string $text): string => $text); + $this->logger = $this->createMock(LoggerInterface::class); } public function testMapSignatureValidationWithEnumState(): void { @@ -75,6 +83,32 @@ public function testMapReasonKeepsUnknownReasonUntouched(): void { $this->assertSame('custom runtime detail', $result['reason']); } + public function testValidateFromStringReturnsEmptyListForUnsignedPdfException(): void { + $this->logger->expects($this->never())->method('warning'); + + $service = new class($this->appConfig, $this->l10n, $this->logger) extends PdfSignatureValidationService { + protected function validateNativeFromString(string $pdfContent): array { + throw new UnsignedPdfException('Unsigned file.'); + } + }; + + $this->assertSame([], $service->validateFromString('%PDF-1.7')); + } + + public function testValidateFromStringPropagatesUnexpectedRuntimeException(): void { + $this->logger->expects($this->never())->method('warning'); + + $service = new class($this->appConfig, $this->l10n, $this->logger) extends PdfSignatureValidationService { + protected function validateNativeFromString(string $pdfContent): array { + throw new \RuntimeException('validator boom'); + } + }; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('validator boom'); + $service->validateFromString('%PDF-1.7'); + } + private function newServiceWithoutConstructor(): PdfSignatureValidationService { $reflection = new \ReflectionClass(PdfSignatureValidationService::class); /** @var PdfSignatureValidationService $service */ From 0c9ffc094ae1ec9a3cfbb83c992067d1f4e97784 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:43:15 -0300 Subject: [PATCH 28/35] test: cover metadata extraction failure propagation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Handler/SignEngine/Pkcs12HandlerTest.php | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 1d804a0424..e75cff78d2 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -511,4 +511,58 @@ public function testGetCertificateChainDoesNotOverrideLegacySignatureValidationO $this->assertSame('Signature is valid.', $result[0]['chain'][0]['signature_validation']['label']); } + public function testGetCertificateChainPropagatesUnexpectedNativeMetadataExtractionFailureAndResetsPolicyValidationContext(): void { + $this->logger->expects($this->never())->method('warning'); + + $policyCalls = []; + $certificateEngine = $this->createMock(IEngineHandler::class); + $certificateEngine->method('setPolicyUserIdForValidation') + ->willReturnCallback(function (?string $userId) use (&$policyCalls, $certificateEngine) { + $policyCalls[] = $userId; + return $certificateEngine; + }); + + $certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); + $certificateEngineFactory->method('getEngine')->willReturn($certificateEngine); + + $handler = new class( + $this->folderService, + $this->appConfig, + $certificateEngineFactory, + $this->l10n, + $this->footerHandler, + $this->logger, + $this->caIdentifierService, + $this->docMdpHandler, + $this->crlService, + $this->pdfSignatureValidationService, + $this->pdfSignatureExtractor, + ) extends Pkcs12Handler { + protected function extractNativeSignaturesFromContent(string $content): array { + throw new \RuntimeException('metadata boom'); + } + }; + + $handler->setPolicyUserIdForValidation('requester'); + $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); + $this->assertIsResource($resource); + + $thrown = null; + try { + $handler->getCertificateChain($resource); + $this->fail('Expected RuntimeException to be propagated.'); + } catch (\RuntimeException $exception) { + $thrown = $exception; + } finally { + fclose($resource); + } + + $this->assertInstanceOf(\RuntimeException::class, $thrown); + $this->assertSame('metadata boom', $thrown->getMessage()); + $this->assertSame(['requester', null], $policyCalls); + + $reflection = new \ReflectionProperty(Pkcs12Handler::class, 'policyUserIdForValidation'); + $this->assertNull($reflection->getValue($handler)); + } + } From 0d7674a19ce5cfcc2abf47ae5aa4d08efffa6e05 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:47:30 -0300 Subject: [PATCH 29/35] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Unit/Handler/SignEngine/Pkcs12HandlerTest.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index e75cff78d2..3f8c0e48bb 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -525,19 +525,7 @@ public function testGetCertificateChainPropagatesUnexpectedNativeMetadataExtract $certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); $certificateEngineFactory->method('getEngine')->willReturn($certificateEngine); - $handler = new class( - $this->folderService, - $this->appConfig, - $certificateEngineFactory, - $this->l10n, - $this->footerHandler, - $this->logger, - $this->caIdentifierService, - $this->docMdpHandler, - $this->crlService, - $this->pdfSignatureValidationService, - $this->pdfSignatureExtractor, - ) extends Pkcs12Handler { + $handler = new class($this->folderService, $this->appConfig, $certificateEngineFactory, $this->l10n, $this->footerHandler, $this->logger, $this->caIdentifierService, $this->docMdpHandler, $this->crlService, $this->pdfSignatureValidationService, $this->pdfSignatureExtractor, ) extends Pkcs12Handler { protected function extractNativeSignaturesFromContent(string $content): array { throw new \RuntimeException('metadata boom'); } From 48732b000bdcb4042ead78a525d3e5175f07eeba Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:58:25 -0300 Subject: [PATCH 30/35] fix: unit tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> From 369487dad4a8ea1091e6776c9331d85f7feddf96 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:13:46 -0300 Subject: [PATCH 31/35] fix(sign-engine): backport policy validation context Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../CertificateEngine/AEngineHandler.php | 20 ++++++++++++++----- .../CertificateEngine/IEngineHandler.php | 2 ++ lib/Handler/SignEngine/SignEngineHandler.php | 7 +++++-- lib/Service/Crl/CrlRevocationChecker.php | 6 +++--- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/lib/Handler/CertificateEngine/AEngineHandler.php b/lib/Handler/CertificateEngine/AEngineHandler.php index 093f5ed113..ee76985eed 100644 --- a/lib/Handler/CertificateEngine/AEngineHandler.php +++ b/lib/Handler/CertificateEngine/AEngineHandler.php @@ -75,6 +75,7 @@ abstract class AEngineHandler implements IEngineHandler { protected string $currentCaId = ''; protected IAppData $appData; private CrlDistributionPointsExtractor $crlDistributionPointsExtractor; + private ?string $policyUserIdForValidation = null; public function __construct( protected IConfig $config, @@ -161,6 +162,15 @@ public function parseCertificate(string $certificate): array { return $this->parseX509($certificate); } + #[\Override] + public function setPolicyUserIdForValidation(?string $userId): self { + $this->policyUserIdForValidation = is_string($userId) && trim($userId) !== '' + ? trim($userId) + : null; + + return $this; + } + private function parseX509(string $x509): array { $parsed = openssl_x509_parse(openssl_x509_read($x509)); @@ -195,7 +205,7 @@ private function addCrlValidationInfo(array &$certData, string $certPem): void { return; } - $crlDetails = $this->crlRevocationChecker->validate($extractedUrls, $certPem); + $crlDetails = $this->crlRevocationChecker->validate($extractedUrls, $certPem, $this->policyUserIdForValidation); $certData['crl_validation'] = $crlDetails['status']; if (!empty($crlDetails['revoked_at'])) { $certData['crl_revoked_at'] = $crlDetails['revoked_at']; @@ -204,10 +214,10 @@ private function addCrlValidationInfo(array &$certData, string $certPem): void { } } - $externalValidationEnabled = $this->appConfig->getValueBool(Application::APP_ID, 'crl_external_validation_enabled', true); - $certData['crl_validation'] = $externalValidationEnabled - ? CrlValidationStatus::MISSING - : CrlValidationStatus::DISABLED; + $emptyCrlValidation = $this->crlRevocationChecker->validate([], $certPem, $this->policyUserIdForValidation); + $certData['crl_validation'] = ($emptyCrlValidation['status'] ?? CrlValidationStatus::NO_URLS) === CrlValidationStatus::DISABLED + ? CrlValidationStatus::DISABLED + : CrlValidationStatus::MISSING; $certData['crl_urls'] = []; } diff --git a/lib/Handler/CertificateEngine/IEngineHandler.php b/lib/Handler/CertificateEngine/IEngineHandler.php index 2f9073c31b..254ee5d270 100644 --- a/lib/Handler/CertificateEngine/IEngineHandler.php +++ b/lib/Handler/CertificateEngine/IEngineHandler.php @@ -78,6 +78,8 @@ public function toArray(): array; */ public function generateCrlDer(array $revokedCertificates, string $instanceId, int $generation, int $crlNumber): string; + public function setPolicyUserIdForValidation(?string $userId): self; + /** * Parse an X.509 certificate and return its details with CRL validation * @param string $certificate PEM-encoded certificate diff --git a/lib/Handler/SignEngine/SignEngineHandler.php b/lib/Handler/SignEngine/SignEngineHandler.php index c3b3d249a9..500625fba4 100644 --- a/lib/Handler/SignEngine/SignEngineHandler.php +++ b/lib/Handler/SignEngine/SignEngineHandler.php @@ -262,9 +262,12 @@ protected function getFileStream() { return $stream; } + protected function getCertificateEngineFactory(): CertificateEngineFactory { + return \OCP\Server::get(CertificateEngineFactory::class); + } + protected function getCertificateEngine(): IEngineHandler { - return \OCP\Server::get(CertificateEngineFactory::class) - ->getEngine(); + return $this->getCertificateEngineFactory()->getEngine(); } protected function beforeSign(): void { diff --git a/lib/Service/Crl/CrlRevocationChecker.php b/lib/Service/Crl/CrlRevocationChecker.php index 727ad962d7..6324c46075 100644 --- a/lib/Service/Crl/CrlRevocationChecker.php +++ b/lib/Service/Crl/CrlRevocationChecker.php @@ -56,8 +56,8 @@ public function __construct( * * @return array{status: CrlValidationStatus, revoked_at?: string} */ - public function validate(array $crlUrls, string $certPem): array { - return $this->validateFromUrlsWithDetails($crlUrls, $certPem); + public function validate(array $crlUrls, string $certPem, ?string $policyUserId = null): array { + return $this->validateFromUrlsWithDetails($crlUrls, $certPem, $policyUserId); } /** @@ -66,7 +66,7 @@ public function validate(array $crlUrls, string $certPem): array { * * @return array{status: CrlValidationStatus, revoked_at?: string} */ - private function validateFromUrlsWithDetails(array $crlUrls, string $certPem): array { + private function validateFromUrlsWithDetails(array $crlUrls, string $certPem, ?string $policyUserId = null): array { $externalValidationEnabled = $this->appConfig->getValueBool(Application::APP_ID, 'crl_external_validation_enabled', true); if (empty($crlUrls)) { From bf65c5dbaecd43cf707257f6105c25b71d6dea79 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:13:54 -0300 Subject: [PATCH 32/35] fix(sign-engine): restore native validation flow Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 54 ++++++++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 031e84e0a6..594c5b014c 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -36,6 +36,7 @@ class Pkcs12Handler extends SignEngineHandler { private ?PhpNativeHandler $phpNativeHandler = null; private string $rootCertificatePem = ''; private bool $isLibreSignFile = false; + private ?string $policyUserIdForValidation = null; public function __construct( private FolderService $folderService, @@ -53,6 +54,11 @@ public function __construct( parent::__construct($l10n, $folderService, $logger); } + #[\Override] + protected function getCertificateEngineFactory(): CertificateEngineFactory { + return $this->certificateEngineFactory; + } + /** * @throws LibresignException When is not a signed file */ @@ -88,6 +94,14 @@ public function setIsLibreSignFile(): void { $this->isLibreSignFile = true; } + public function setPolicyUserIdForValidation(?string $userId): self { + $this->policyUserIdForValidation = is_string($userId) && trim($userId) !== '' + ? trim($userId) + : null; + + return $this; + } + /** * @param resource $resource * @throws LibresignException When is not a signed file @@ -96,20 +110,42 @@ public function setIsLibreSignFile(): void { #[\Override] public function getCertificateChain($resource): array { $certificates = []; + $certificateEngine = $this->getCertificateEngine(); + $certificateEngine->setPolicyUserIdForValidation($this->policyUserIdForValidation); - foreach ($this->getSignatures($resource) as $signature) { - if (!$signature) { - continue; - } + try { + $nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource)); + rewind($resource); + $nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource)); + $index = 0; + + foreach ($this->getSignatures($resource) as $signature) { + $metadata = $nativeMetadata[$index] ?? []; + $validation = $nativeValidation[$index] ?? []; + $index++; + + if (!$signature) { + continue; + } - $result = $this->processSignature($resource, $signature); + $result = $this->processSignature( + $resource, + $signature, + $metadata, + $validation + ); - if (empty($result['chain'])) { - continue; - } + if (empty($result['chain'])) { + continue; + } - $certificates[] = $result; + $certificates[] = $result; + } + } finally { + $certificateEngine->setPolicyUserIdForValidation(null); + $this->policyUserIdForValidation = null; } + return $certificates; } From f844a3cace9d877d13e990435298762f974be208 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:14:00 -0300 Subject: [PATCH 33/35] test(sign-engine): cover stable native validation flow Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Handler/SignEngine/Pkcs12HandlerTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 3f8c0e48bb..2569278191 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -11,6 +11,7 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; +use OCA\Libresign\Handler\CertificateEngine\IEngineHandler; use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\FooterHandler; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; @@ -37,16 +38,35 @@ final class Pkcs12HandlerTest extends \OCA\Libresign\Tests\Unit\TestCase { private FooterHandler&MockObject $footerHandler; private LoggerInterface&MockObject $logger; private CertificateEngineFactory&MockObject $certificateEngineFactory; + private IEngineHandler&MockObject $certificateEngine; private CaIdentifierService&MockObject $caIdentifierService; private DocMdpHandler&MockObject $docMdpHandler; private CrlService&MockObject $crlService; private PdfSignatureValidationService&MockObject $pdfSignatureValidationService; private PdfSignatureExtractor $pdfSignatureExtractor; + #[\Override] public function setUp(): void { $this->folderService = $this->createMock(FolderService::class); $this->appConfig = $this->getMockAppConfigWithReset(); $this->certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); + $this->certificateEngine = $this->createMock(IEngineHandler::class); + $this->certificateEngine->method('setPolicyUserIdForValidation')->willReturnSelf(); + $this->certificateEngine + ->method('parseCertificate') + ->willReturnCallback(static function (string $certificate): array { + $resource = openssl_x509_read($certificate); + if ($resource === false) { + return []; + } + + $parsed = openssl_x509_parse($resource); + return is_array($parsed) ? $parsed : []; + }); + $this->certificateEngine->method('isSetupOk')->willReturn(true); + $this->certificateEngine->method('validateRootCertificate')->willReturnCallback(static function (): void { + }); + $this->certificateEngineFactory->method('getEngine')->willReturn($this->certificateEngine); $this->l10n = \OCP\Server::get(IL10NFactory::class)->get(Application::APP_ID); $this->footerHandler = $this->createMock(FooterHandler::class); $this->logger = $this->createMock(LoggerInterface::class); From f9b096f42407d48c9f671925d11fee3a0fea7692 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:35:17 -0300 Subject: [PATCH 34/35] Revert "fix(sign-engine): backport policy validation context" This reverts commit 369487dad4a8ea1091e6776c9331d85f7feddf96. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../CertificateEngine/AEngineHandler.php | 20 +++++-------------- .../CertificateEngine/IEngineHandler.php | 2 -- lib/Handler/SignEngine/SignEngineHandler.php | 7 ++----- lib/Service/Crl/CrlRevocationChecker.php | 6 +++--- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/lib/Handler/CertificateEngine/AEngineHandler.php b/lib/Handler/CertificateEngine/AEngineHandler.php index ee76985eed..093f5ed113 100644 --- a/lib/Handler/CertificateEngine/AEngineHandler.php +++ b/lib/Handler/CertificateEngine/AEngineHandler.php @@ -75,7 +75,6 @@ abstract class AEngineHandler implements IEngineHandler { protected string $currentCaId = ''; protected IAppData $appData; private CrlDistributionPointsExtractor $crlDistributionPointsExtractor; - private ?string $policyUserIdForValidation = null; public function __construct( protected IConfig $config, @@ -162,15 +161,6 @@ public function parseCertificate(string $certificate): array { return $this->parseX509($certificate); } - #[\Override] - public function setPolicyUserIdForValidation(?string $userId): self { - $this->policyUserIdForValidation = is_string($userId) && trim($userId) !== '' - ? trim($userId) - : null; - - return $this; - } - private function parseX509(string $x509): array { $parsed = openssl_x509_parse(openssl_x509_read($x509)); @@ -205,7 +195,7 @@ private function addCrlValidationInfo(array &$certData, string $certPem): void { return; } - $crlDetails = $this->crlRevocationChecker->validate($extractedUrls, $certPem, $this->policyUserIdForValidation); + $crlDetails = $this->crlRevocationChecker->validate($extractedUrls, $certPem); $certData['crl_validation'] = $crlDetails['status']; if (!empty($crlDetails['revoked_at'])) { $certData['crl_revoked_at'] = $crlDetails['revoked_at']; @@ -214,10 +204,10 @@ private function addCrlValidationInfo(array &$certData, string $certPem): void { } } - $emptyCrlValidation = $this->crlRevocationChecker->validate([], $certPem, $this->policyUserIdForValidation); - $certData['crl_validation'] = ($emptyCrlValidation['status'] ?? CrlValidationStatus::NO_URLS) === CrlValidationStatus::DISABLED - ? CrlValidationStatus::DISABLED - : CrlValidationStatus::MISSING; + $externalValidationEnabled = $this->appConfig->getValueBool(Application::APP_ID, 'crl_external_validation_enabled', true); + $certData['crl_validation'] = $externalValidationEnabled + ? CrlValidationStatus::MISSING + : CrlValidationStatus::DISABLED; $certData['crl_urls'] = []; } diff --git a/lib/Handler/CertificateEngine/IEngineHandler.php b/lib/Handler/CertificateEngine/IEngineHandler.php index 254ee5d270..2f9073c31b 100644 --- a/lib/Handler/CertificateEngine/IEngineHandler.php +++ b/lib/Handler/CertificateEngine/IEngineHandler.php @@ -78,8 +78,6 @@ public function toArray(): array; */ public function generateCrlDer(array $revokedCertificates, string $instanceId, int $generation, int $crlNumber): string; - public function setPolicyUserIdForValidation(?string $userId): self; - /** * Parse an X.509 certificate and return its details with CRL validation * @param string $certificate PEM-encoded certificate diff --git a/lib/Handler/SignEngine/SignEngineHandler.php b/lib/Handler/SignEngine/SignEngineHandler.php index 500625fba4..c3b3d249a9 100644 --- a/lib/Handler/SignEngine/SignEngineHandler.php +++ b/lib/Handler/SignEngine/SignEngineHandler.php @@ -262,12 +262,9 @@ protected function getFileStream() { return $stream; } - protected function getCertificateEngineFactory(): CertificateEngineFactory { - return \OCP\Server::get(CertificateEngineFactory::class); - } - protected function getCertificateEngine(): IEngineHandler { - return $this->getCertificateEngineFactory()->getEngine(); + return \OCP\Server::get(CertificateEngineFactory::class) + ->getEngine(); } protected function beforeSign(): void { diff --git a/lib/Service/Crl/CrlRevocationChecker.php b/lib/Service/Crl/CrlRevocationChecker.php index 6324c46075..727ad962d7 100644 --- a/lib/Service/Crl/CrlRevocationChecker.php +++ b/lib/Service/Crl/CrlRevocationChecker.php @@ -56,8 +56,8 @@ public function __construct( * * @return array{status: CrlValidationStatus, revoked_at?: string} */ - public function validate(array $crlUrls, string $certPem, ?string $policyUserId = null): array { - return $this->validateFromUrlsWithDetails($crlUrls, $certPem, $policyUserId); + public function validate(array $crlUrls, string $certPem): array { + return $this->validateFromUrlsWithDetails($crlUrls, $certPem); } /** @@ -66,7 +66,7 @@ public function validate(array $crlUrls, string $certPem, ?string $policyUserId * * @return array{status: CrlValidationStatus, revoked_at?: string} */ - private function validateFromUrlsWithDetails(array $crlUrls, string $certPem, ?string $policyUserId = null): array { + private function validateFromUrlsWithDetails(array $crlUrls, string $certPem): array { $externalValidationEnabled = $this->appConfig->getValueBool(Application::APP_ID, 'crl_external_validation_enabled', true); if (empty($crlUrls)) { From d6c68acffff6d25557d0af0d28a42f40d73ceba7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:36:10 -0300 Subject: [PATCH 35/35] fix(backport): drop policy context from stable33 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 64 ++++++------------- .../Handler/SignEngine/Pkcs12HandlerTest.php | 61 ------------------ 2 files changed, 21 insertions(+), 104 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 594c5b014c..cbfde44872 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -36,7 +36,6 @@ class Pkcs12Handler extends SignEngineHandler { private ?PhpNativeHandler $phpNativeHandler = null; private string $rootCertificatePem = ''; private bool $isLibreSignFile = false; - private ?string $policyUserIdForValidation = null; public function __construct( private FolderService $folderService, @@ -54,11 +53,6 @@ public function __construct( parent::__construct($l10n, $folderService, $logger); } - #[\Override] - protected function getCertificateEngineFactory(): CertificateEngineFactory { - return $this->certificateEngineFactory; - } - /** * @throws LibresignException When is not a signed file */ @@ -94,14 +88,6 @@ public function setIsLibreSignFile(): void { $this->isLibreSignFile = true; } - public function setPolicyUserIdForValidation(?string $userId): self { - $this->policyUserIdForValidation = is_string($userId) && trim($userId) !== '' - ? trim($userId) - : null; - - return $this; - } - /** * @param resource $resource * @throws LibresignException When is not a signed file @@ -110,40 +96,32 @@ public function setPolicyUserIdForValidation(?string $userId): self { #[\Override] public function getCertificateChain($resource): array { $certificates = []; - $certificateEngine = $this->getCertificateEngine(); - $certificateEngine->setPolicyUserIdForValidation($this->policyUserIdForValidation); + $nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource)); + rewind($resource); + $nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource)); + $index = 0; - try { - $nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource)); - rewind($resource); - $nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource)); - $index = 0; - - foreach ($this->getSignatures($resource) as $signature) { - $metadata = $nativeMetadata[$index] ?? []; - $validation = $nativeValidation[$index] ?? []; - $index++; - - if (!$signature) { - continue; - } + foreach ($this->getSignatures($resource) as $signature) { + $metadata = $nativeMetadata[$index] ?? []; + $validation = $nativeValidation[$index] ?? []; + $index++; - $result = $this->processSignature( - $resource, - $signature, - $metadata, - $validation - ); + if (!$signature) { + continue; + } - if (empty($result['chain'])) { - continue; - } + $result = $this->processSignature( + $resource, + $signature, + $metadata, + $validation + ); - $certificates[] = $result; + if (empty($result['chain'])) { + continue; } - } finally { - $certificateEngine->setPolicyUserIdForValidation(null); - $this->policyUserIdForValidation = null; + + $certificates[] = $result; } return $certificates; diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 2569278191..8cddfd9a11 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -11,7 +11,6 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; -use OCA\Libresign\Handler\CertificateEngine\IEngineHandler; use OCA\Libresign\Handler\DocMdpHandler; use OCA\Libresign\Handler\FooterHandler; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; @@ -38,7 +37,6 @@ final class Pkcs12HandlerTest extends \OCA\Libresign\Tests\Unit\TestCase { private FooterHandler&MockObject $footerHandler; private LoggerInterface&MockObject $logger; private CertificateEngineFactory&MockObject $certificateEngineFactory; - private IEngineHandler&MockObject $certificateEngine; private CaIdentifierService&MockObject $caIdentifierService; private DocMdpHandler&MockObject $docMdpHandler; private CrlService&MockObject $crlService; @@ -50,23 +48,6 @@ public function setUp(): void { $this->folderService = $this->createMock(FolderService::class); $this->appConfig = $this->getMockAppConfigWithReset(); $this->certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); - $this->certificateEngine = $this->createMock(IEngineHandler::class); - $this->certificateEngine->method('setPolicyUserIdForValidation')->willReturnSelf(); - $this->certificateEngine - ->method('parseCertificate') - ->willReturnCallback(static function (string $certificate): array { - $resource = openssl_x509_read($certificate); - if ($resource === false) { - return []; - } - - $parsed = openssl_x509_parse($resource); - return is_array($parsed) ? $parsed : []; - }); - $this->certificateEngine->method('isSetupOk')->willReturn(true); - $this->certificateEngine->method('validateRootCertificate')->willReturnCallback(static function (): void { - }); - $this->certificateEngineFactory->method('getEngine')->willReturn($this->certificateEngine); $this->l10n = \OCP\Server::get(IL10NFactory::class)->get(Application::APP_ID); $this->footerHandler = $this->createMock(FooterHandler::class); $this->logger = $this->createMock(LoggerInterface::class); @@ -531,46 +512,4 @@ public function testGetCertificateChainDoesNotOverrideLegacySignatureValidationO $this->assertSame('Signature is valid.', $result[0]['chain'][0]['signature_validation']['label']); } - public function testGetCertificateChainPropagatesUnexpectedNativeMetadataExtractionFailureAndResetsPolicyValidationContext(): void { - $this->logger->expects($this->never())->method('warning'); - - $policyCalls = []; - $certificateEngine = $this->createMock(IEngineHandler::class); - $certificateEngine->method('setPolicyUserIdForValidation') - ->willReturnCallback(function (?string $userId) use (&$policyCalls, $certificateEngine) { - $policyCalls[] = $userId; - return $certificateEngine; - }); - - $certificateEngineFactory = $this->createMock(CertificateEngineFactory::class); - $certificateEngineFactory->method('getEngine')->willReturn($certificateEngine); - - $handler = new class($this->folderService, $this->appConfig, $certificateEngineFactory, $this->l10n, $this->footerHandler, $this->logger, $this->caIdentifierService, $this->docMdpHandler, $this->crlService, $this->pdfSignatureValidationService, $this->pdfSignatureExtractor, ) extends Pkcs12Handler { - protected function extractNativeSignaturesFromContent(string $content): array { - throw new \RuntimeException('metadata boom'); - } - }; - - $handler->setPolicyUserIdForValidation('requester'); - $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); - $this->assertIsResource($resource); - - $thrown = null; - try { - $handler->getCertificateChain($resource); - $this->fail('Expected RuntimeException to be propagated.'); - } catch (\RuntimeException $exception) { - $thrown = $exception; - } finally { - fclose($resource); - } - - $this->assertInstanceOf(\RuntimeException::class, $thrown); - $this->assertSame('metadata boom', $thrown->getMessage()); - $this->assertSame(['requester', null], $policyCalls); - - $reflection = new \ReflectionProperty(Pkcs12Handler::class, 'policyUserIdForValidation'); - $this->assertNull($reflection->getValue($handler)); - } - }