From a7ca2e1c28182decee61a27a7feb6a3aa7dd5915 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:18:29 -0300 Subject: [PATCH 01/76] chore: update pdf-signature-validator Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- 3rdparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty b/3rdparty index cddfc39be9..c5f453b5e3 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit cddfc39be9e5841421ffb954f919afc2ff6a9809 +Subproject commit c5f453b5e3d514ff7b84daca9332a7a31e0bcfa1 From 4cd63b50854dac19a2843d62689229deb17b971c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:10 -0300 Subject: [PATCH 02/76] refactor(validation): expose PDF validator package data Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 2a222deb4c..51650461d5 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -10,6 +10,8 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature; +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\PdfSignatureValidator; @@ -126,14 +128,27 @@ private function mapValidationResults(array $results): array { $mapped = []; foreach ($results as $result) { + $signature = $result['signature'] ?? null; $sigValidation = $result['signatureValidation'] ?? null; $certValidation = $result['certificateValidation'] ?? null; - if (!$sigValidation instanceof ValidationResult || !$certValidation instanceof ValidationResult) { + if ( + !$signature instanceof ExtractedSignature + || !$sigValidation instanceof ValidationResult + || !$certValidation instanceof ValidationResult + ) { continue; } + $certificates = $result['certificates'] ?? []; + if (!is_array($certificates)) { + $certificates = []; + } + $mapped[] = [ + 'signature' => $signature, + 'certificates' => array_values($certificates), + 'timestamp' => $result['timestamp'] ?? null, 'signatureValidation' => $this->mapSignatureValidation($sigValidation), 'certificateValidation' => $this->mapCertificateValidation($certValidation), 'raw' => [ @@ -158,28 +173,28 @@ private function mapSignatureValidation(ValidationResult $result): array { 'id' => 2, // TRANSLATORS User-facing status when signature cryptographic validation fails. 'label' => $this->l10n->t('Signature is invalid.'), - 'reason' => $this->translateKnownReason($result->reason), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), 'isValid' => false, ], }; @@ -197,47 +212,60 @@ private function mapCertificateValidation(ValidationResult $result): array { '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), '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), + 'reason' => $this->translateValidationReason($result), 'isValid' => false, ], }; } + private function translateValidationReason(ValidationResult $result): ?string { + if ($result->reasonCode !== null) { + return match ($result->reasonCode) { + ValidationReason::NO_BYTE_RANGE => $this->l10n->t('No ByteRange in signature'), + ValidationReason::DIGEST_MISMATCH => $this->l10n->t('PDF content hash does not match signed digest'), + ValidationReason::NO_BINARY_SIGNATURE => $this->l10n->t('No binary signature'), + ValidationReason::SIGNATURE_CERTIFICATE_MISMATCH => $this->l10n->t('Signature does not match certificate'), + }; + } + + return $this->translateKnownReason($result->reason); + } + private function translateKnownReason(?string $reason): ?string { if ($reason === null || $reason === '') { return $reason; From c1a18c92be92530c4264013e713efdc03193b6d7 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:18 -0300 Subject: [PATCH 03/76] refactor(signing): remove legacy PDF signature parsing Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 240 ++++++++++------------- 1 file changed, 107 insertions(+), 133 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 4d0ac369ae..f34efb11da 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -18,8 +18,8 @@ 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\Parser\PdfSignatureExtractor; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken; use OCA\Libresign\Vendor\phpseclib4\Exception\UnexpectedValueException; use OCA\Libresign\Vendor\phpseclib4\File\ASN1; use OCP\Files\File; @@ -47,7 +47,6 @@ public function __construct( private DocMdpHandler $docMdpHandler, private CrlService $crlService, private PdfSignatureValidationService $pdfSignatureValidationService, - private PdfSignatureExtractor $pdfSignatureExtractor, ) { parent::__construct($l10n, $folderService, $logger); } @@ -57,38 +56,6 @@ protected function getCertificateEngineFactory(): CertificateEngineFactory { return $this->certificateEngineFactory; } - /** - * @throws LibresignException When is not a signed file - */ - private function getSignatures($resource): iterable { - rewind($resource); - $content = stream_get_contents($resource); - - preg_match_all('/\/Contents\s*<([0-9a-fA-F]+)>/', $content, $contents, PREG_OFFSET_CAPTURE); - - if (empty($contents[1])) { - // TRANSLATORS Error while LibreSign reads a PDF for signature validation: the file has no embedded PKCS#12/PDF signature bytes yet. - throw new LibresignException($this->l10n->t('Unsigned file.')); - } - - $seenHexSignatures = []; - foreach ($contents[1] as $match) { - $signatureHex = $match[0]; - - if (isset($seenHexSignatures[$signatureHex])) { - continue; - } - $seenHexSignatures[$signatureHex] = true; - - $decodedSignature = @hex2bin($signatureHex); - if ($decodedSignature === false) { - yield null; - continue; - } - yield $decodedSignature; - } - } - public function setIsLibreSignFile(): void { $this->isLibreSignFile = true; } @@ -113,25 +80,25 @@ public function getCertificateChain($resource): array { $certificateEngine->setPolicyUserIdForValidation($this->policyUserIdForValidation); try { - $nativeMetadata = array_values($this->extractNativeSignatureMetadata($resource)); rewind($resource); - $nativeValidation = array_values($this->pdfSignatureValidationService->validateFromResource($resource)); - $index = 0; + $validationResults = array_values( + $this->pdfSignatureValidationService->validateFromResource($resource) + ); - foreach ($this->getSignatures($resource) as $signature) { - $metadata = $nativeMetadata[$index] ?? []; - $validation = $nativeValidation[$index] ?? []; - $index++; + if ($validationResults === []) { + throw new LibresignException($this->l10n->t('Unsigned file.')); + } - if (!$signature) { + foreach ($validationResults as $validation) { + $signature = $validation['signature'] ?? null; + if (!$signature instanceof ExtractedSignature) { continue; } $result = $this->processSignature( $resource, $signature, - $metadata, - $validation + $validation, ); if (empty($result['chain'])) { @@ -148,35 +115,49 @@ public function getCertificateChain($resource): array { return $certificates; } - private function processSignature($resource, ?string $signature, array $metadata = [], array $validation = []): array { - $result = []; - - if (!$signature) { - $result['chain'][0]['signature_validation'] = [ - 'id' => 3, - // TRANSLATORS Status label on LibreSign's public/document validation UI when the PDF signature hash does not match the document bytes (tamper or corrupt signature). - 'label' => $this->l10n->t('Digest mismatch.'), - ]; - return $result; + private function processSignature( + $resource, + ExtractedSignature $signature, + array $validation = [], + ): array { + $binarySignature = $signature->binarySignature; + if ($binarySignature === null || $binarySignature === '') { + return []; } + $result = []; + try { - $decoded = ASN1::decodeBER($signature); + $decoded = ASN1::decodeBER($binarySignature); } catch (UnexpectedValueException) { - return []; + $decoded = null; + } + + $result = $this->extractSigningTime($decoded, $result); + + $timestamp = $validation['timestamp'] ?? null; + if ($timestamp instanceof TimestampToken) { + $result['timestamp'] = $this->mapTimestampToken($timestamp); + } + + $pemCertificates = $validation['certificates'] ?? []; + if (!is_array($pemCertificates)) { + $pemCertificates = []; } - $result = $this->extractTimestampData($decoded, $result); - $chain = $this->extractCertificateChain($signature); + $chain = $this->extractCertificateChain($pemCertificates); if (!empty($chain)) { $result['chain'] = $this->orderCertificates($chain); - $result = $this->enrichLeafWithNativeData($result, $metadata, $validation); + $result = $this->enrichLeafWithNativeData( + $result, + $signature, + $validation, + ); } $result = $this->extractDocMdpData($resource, $result); - $result = $this->applyLibreSignRootCAFlag($result); - return $result; + return $this->applyLibreSignRootCAFlag($result); } private function applyLibreSignRootCAFlag(array $signer): array { @@ -209,55 +190,72 @@ private function extractDocMdpData($resource, array $result): array { return array_merge($result, $docMdpData); } - private function extractTimestampData(?array $decoded, array $result): array { + private function extractSigningTime(?array $decoded, array $result): array { if ($decoded === null) { return $result; } $tsa = new TSA(); - - $timestampData = $tsa->extract($decoded); - if (!empty($timestampData['genTime']) || !empty($timestampData['policy']) || !empty($timestampData['serialNumber'])) { - $result['timestamp'] = $timestampData; + $signingTime = $tsa->getSigninTime($decoded); + if ($signingTime instanceof \DateTime) { + $result['signingTime'] = $signingTime; } - if (!isset($result['signingTime']) || !$result['signingTime'] instanceof \DateTime) { - $result['signingTime'] = $tsa->getSigninTime($decoded); - } return $result; } - private function extractCertificateChain(string $signature): array { - $pkcs7PemSignature = $this->der2pem($signature); - $pemCertificates = []; + private function mapTimestampToken(TimestampToken $timestamp): array { + $result = [ + 'genTime' => $timestamp->generatedAt, + 'policy' => $timestamp->policyOid, + 'serialNumber' => $timestamp->serialNumber, + ]; - if (!openssl_pkcs7_read($pkcs7PemSignature, $pemCertificates)) { - return []; + $commonName = $timestamp->certificateSubject['CN'] ?? null; + if (is_string($commonName) && $commonName !== '') { + $result['tsaName'] = $commonName; } + return array_filter( + $result, + static fn (mixed $value): bool => $value !== null && $value !== '', + ); + } + + /** + * @param list $pemCertificates + */ + private function extractCertificateChain(array $pemCertificates): array { $chain = []; $isLibreSignRootCA = false; $certificateEngine = $this->getCertificateEngine(); foreach ($pemCertificates as $index => $pemCertificate) { + if (!is_string($pemCertificate) || $pemCertificate === '') { + continue; + } + $parsed = $certificateEngine->parseCertificate($pemCertificate); - if ($parsed) { - $parsed['signature_validation'] = [ - 'id' => 1, - // TRANSLATORS Status label on LibreSign signature validation when the cryptographic PDF signature checks out successfully. - 'label' => $this->l10n->t('Signature is valid.'), - ]; - if (!$isLibreSignRootCA) { - $isLibreSignRootCA = $this->isLibreSignRootCA($pemCertificate, $parsed); - } - $parsed['isLibreSignRootCA'] = $isLibreSignRootCA; - $chain[$index] = $parsed; + if (!$parsed) { + continue; } + + if (!$isLibreSignRootCA) { + $isLibreSignRootCA = $this->isLibreSignRootCA( + $pemCertificate, + $parsed, + ); + } + + $parsed['isLibreSignRootCA'] = $isLibreSignRootCA; + $chain[$index] = $parsed; } + if ($isLibreSignRootCA || $this->isLibreSignFile) { foreach ($chain as &$cert) { $cert['isLibreSignRootCA'] = true; } + unset($cert); } return $chain; @@ -324,24 +322,40 @@ private function getRootCertificatePem(): string { return $this->rootCertificatePem; } - private function enrichLeafWithNativeData(array $result, array $metadata, array $validation): array { + private function enrichLeafWithNativeData( + array $result, + ExtractedSignature $signature, + array $validation, + ): array { if (empty($result['chain'])) { return $result; } $leaf = &$result['chain'][0]; + $metadata = $signature->metadata; - foreach (['field', 'range', 'signature_type', 'signing_hash_algorithm', 'covers_entire_document'] as $key) { - if (array_key_exists($key, $metadata)) { - $leaf[$key] = $metadata[$key]; - } + $leaf['field'] = $metadata->field; + $leaf['range'] = $metadata->range; + $leaf['signature_type'] = $metadata->signatureType; + $leaf['signing_hash_algorithm'] = $signature->hashAlgorithm; + $leaf['covers_entire_document'] = $metadata->coversEntireDocument; + + if ($metadata->documentModificationState !== null) { + $leaf['document_modification_state'] + = $metadata->documentModificationState->value; } - if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { + if ( + isset($validation['signatureValidation']) + && is_array($validation['signatureValidation']) + ) { $leaf['signature_validation'] = $validation['signatureValidation']; } - if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { + if ( + isset($validation['certificateValidation']) + && is_array($validation['certificateValidation']) + ) { $leaf['certificate_validation'] = $validation['certificateValidation']; } @@ -356,46 +370,6 @@ private function enrichLeafWithNativeData(array $result, array $metadata, array return $result; } - /** - * @param resource $resource - * @return array - */ - private function extractNativeSignatureMetadata($resource): array { - rewind($resource); - $content = stream_get_contents($resource); - if (!is_string($content) || $content === '') { - return []; - } - - try { - $signatures = $this->extractNativeSignaturesFromContent($content); - } catch (UnsignedPdfException) { - return []; - } - $metadata = []; - - 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; - } - - 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"; - return $pem; - } - private function getHandler(): SignEngineHandler { $sign_engine = $this->appConfig->getValueString(Application::APP_ID, 'signature_engine', 'JSignPdf'); $property = lcfirst($sign_engine) . 'Handler'; From b2b200da79313e765d964bbf5bb227f250ea7d31 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:28 -0300 Subject: [PATCH 04/76] feat(validation): expose PDF modification state on signers Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/File/CertificateSignersMergeService.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/Service/File/CertificateSignersMergeService.php b/lib/Service/File/CertificateSignersMergeService.php index 64812f4f3f..c0190290b8 100644 --- a/lib/Service/File/CertificateSignersMergeService.php +++ b/lib/Service/File/CertificateSignersMergeService.php @@ -406,6 +406,14 @@ private function enrichSignerWithCertificateValidation(\stdClass $signer, array if (isset($endEntityCert['covers_entire_document']) && !isset($signer->covers_entire_document)) { $signer->covers_entire_document = $endEntityCert['covers_entire_document']; } + + if ( + isset($endEntityCert['document_modification_state']) + && !isset($signer->document_modification_state) + ) { + $signer->document_modification_state + = $endEntityCert['document_modification_state']; + } } /** From 279bacca9102a2702abd71c40093a0924decdd6f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:35 -0300 Subject: [PATCH 05/76] feat(api): add PDF modification state to signer response Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/ResponseDefinitions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 0d4c12222d..23ccb4a435 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -223,6 +223,7 @@ * sign_request_uuid?: string, * hash_algorithm?: string, * covers_entire_document?: bool, + * document_modification_state?: 'unchanged'|'unsigned_content'|'trailing_data'|'invalid_byte_range'|'invalid_eof_boundary', * me: bool, * signingOrder?: non-negative-int, * visibleElements: LibresignVisibleElement[], From 69f93b78349a100716617a0761734a7dd802f280 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:44 -0300 Subject: [PATCH 06/76] feat(validation): show PDF modification states Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 37 +++++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index eae54fa872..d0856666b8 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -98,12 +98,12 @@ {{ getSignatureValidationMessage(signer) }} - + @@ -322,6 +322,7 @@ type SignerModel = { signature_validation?: ValidationState certificate_validation?: ValidationState covers_entire_document?: boolean + document_modification_state?: 'unchanged' | 'unsigned_content' | 'trailing_data' | 'invalid_byte_range' | 'invalid_eof_boundary' crl_validation?: string crl_revoked_at?: string docmdp?: SignerDocMdp @@ -418,6 +419,7 @@ function isRevokedBeforeSigning(signer: SignerModel) { function hasValidationIssues(signer: SignerModel) { return signer.signature_validation?.id !== 1 || signer.certificate_validation?.id !== 1 + || hasDocumentModificationWarning(signer) || isRevokedBeforeSigning(signer) } @@ -441,8 +443,10 @@ function getValidityStatus(signer: SignerModel) { } function hasValidationStatus(signer: SignerModel) { - return !!(signer.signature_validation || signer.certificate_validation || signer.crl_validation - || signer.covers_entire_document === false + return !!(signer.signature_validation + || signer.certificate_validation + || signer.crl_validation + || signer.document_modification_state || (signer.valid_from && signer.valid_to && signer.signed)) } @@ -455,9 +459,28 @@ function getSignatureValidationMessage(signer: SignerModel) { return signer.signature_validation?.message || t('libresign', 'Document integrity check failed') } -function getSignatureCoverageMessage() { - // TRANSLATORS Warning shown when extra bytes exist outside the PDF signature ByteRange. - return t('libresign', 'The signature does not cover the entire document') +function hasDocumentModificationWarning(signer: SignerModel) { + return !!signer.document_modification_state + && signer.document_modification_state !== 'unchanged' +} + +function getDocumentModificationMessage(signer: SignerModel) { + switch (signer.document_modification_state) { + case 'unsigned_content': + // TRANSLATORS Warning shown when content exists after the latest PDF signature. + return t('libresign', 'The document contains unsigned content after the latest signature') + case 'trailing_data': + // TRANSLATORS Warning shown when unexpected bytes exist after the final PDF EOF marker. + return t('libresign', 'Unexpected data was found after the final PDF end marker') + case 'invalid_byte_range': + // TRANSLATORS Warning shown when a PDF signature ByteRange is structurally invalid. + return t('libresign', 'The signature ByteRange is invalid') + case 'invalid_eof_boundary': + // TRANSLATORS Warning shown when signed PDF data does not end at a valid EOF boundary. + return t('libresign', 'The signed content does not end at a valid PDF end marker') + default: + return '' + } } function getCertificateTrustMessage(signer: SignerModel) { From 583b56bbda07d26f1a22deb0e0b98dfda7fe3d51 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:52 -0300 Subject: [PATCH 07/76] test(validation): cover structured PDF validation reasons Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationServiceTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index 137e1e1e9e..d3ec4f0bf7 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -11,6 +11,7 @@ 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\ValidationReason; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; use OCP\IAppConfig; @@ -72,6 +73,24 @@ public function testMapReasonUsesDictionaryForKnownReason(): void { $this->assertSame('PDF content hash does not match signed digest', $result['reason']); } + public function testMapReasonUsesStructuredReasonCode(): void { + $service = $this->newServiceWithoutConstructor(); + $result = $this->invokePrivateMethod( + $service, + 'mapSignatureValidation', + new ValidationResult( + ValidationState::DIGEST_MISMATCH, + 'this text must not be used', + ValidationReason::DIGEST_MISMATCH, + ) + ); + + $this->assertSame( + 'PDF content hash does not match signed digest', + $result['reason'], + ); + } + public function testMapReasonKeepsUnknownReasonUntouched(): void { $service = $this->newServiceWithoutConstructor(); $result = $this->invokePrivateMethod( From db208e9d3f418f59c3f7daf5cc1d06e567767a7e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:22:58 -0300 Subject: [PATCH 08/76] test(validation): cover PDF modification state propagation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Unit/Service/File/CertificateSignersMergeServiceTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php index e7ad50ab76..4121b3d022 100644 --- a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php +++ b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php @@ -163,6 +163,7 @@ public function testMergePromotesLeafCertValidityDatesToSignerRoot(): void { 'chain' => [[ 'subject' => ['CN' => 'Signer User'], 'covers_entire_document' => false, + 'document_modification_state' => 'unsigned_content', 'validFrom_time_t' => 1769644731, 'validTo_time_t' => 1769731131, ]], @@ -185,6 +186,10 @@ public function testMergePromotesLeafCertValidityDatesToSignerRoot(): void { $this->assertSame('2026-01-28T23:58:51+00:00', $signer->chain[0]['valid_from']); $this->assertSame('2026-01-29T23:58:51+00:00', $signer->chain[0]['valid_to']); $this->assertFalse($signer->covers_entire_document); + $this->assertSame( + 'unsigned_content', + $signer->document_modification_state, + ); } public function testMergeDoesNotExportTopLevelTsaWithTimestampData(): void { From a5bafcdb4945010a1ff5d5d08d8288ba737bbe65 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:23:04 -0300 Subject: [PATCH 09/76] test(signing): update PDF validation integration tests Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Handler/SignEngine/Pkcs12HandlerTest.php | 129 +++++++++++------- 1 file changed, 81 insertions(+), 48 deletions(-) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 7f2a30e80e..3580951108 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -20,7 +20,6 @@ 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; @@ -43,8 +42,8 @@ final class Pkcs12HandlerTest extends \OCA\Libresign\Tests\Unit\TestCase { private DocMdpHandler&MockObject $docMdpHandler; private CrlService&MockObject $crlService; private PdfSignatureValidationService&MockObject $pdfSignatureValidationService; - private PdfSignatureExtractor $pdfSignatureExtractor; private array $nativeValidation = []; + private ?\Throwable $nativeValidationException = null; private int $nativeValidationCalls = 0; #[\Override] @@ -77,11 +76,25 @@ public function setUp(): void { $this->crlService = $this->createMock(CrlService::class); $this->pdfSignatureValidationService = $this->createMock(PdfSignatureValidationService::class); $this->pdfSignatureValidationService->method('validateFromResource') - ->willReturnCallback(function (): array { + ->willReturnCallback(function ($resource): array { $this->nativeValidationCalls++; - return $this->nativeValidation; + + if ($this->nativeValidationException !== null) { + throw $this->nativeValidationException; + } + + if ($this->nativeValidation !== []) { + return $this->nativeValidation; + } + + $service = new PdfSignatureValidationService( + $this->appConfig, + $this->l10n, + $this->logger, + ); + + return $service->validateFromResource($resource); }); - $this->pdfSignatureExtractor = new PdfSignatureExtractor(); } private function getHandler(array $methods = []): Pkcs12Handler|MockObject { @@ -98,7 +111,6 @@ private function getHandler(array $methods = []): Pkcs12Handler|MockObject { $this->docMdpHandler, $this->crlService, $this->pdfSignatureValidationService, - $this->pdfSignatureExtractor, ]) ->onlyMethods($methods) ->getMock(); @@ -114,7 +126,6 @@ private function getHandler(array $methods = []): Pkcs12Handler|MockObject { $this->docMdpHandler, $this->crlService, $this->pdfSignatureValidationService, - $this->pdfSignatureExtractor, ); } @@ -435,29 +446,28 @@ 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); + public function testGetCertificateChainProvidesNativePackageShape(): void { + $fixtureResource = fopen( + __DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', + 'r', + ); + $this->assertIsResource($fixtureResource); - $metadata = $signatures[0]->metadata; - $this->assertSame('Signature1', $metadata->field); - $this->assertSame([ - 'offset1' => 0, - 'length1' => 1311, - 'offset2' => 31313, - 'length2' => 32829, - ], $metadata->range); - } + $service = new PdfSignatureValidationService( + $this->appConfig, + $this->l10n, + $this->logger, + ); + $this->nativeValidation = $service->validateFromResource($fixtureResource); + fclose($fixtureResource); - public function testGetCertificateChainProvidesNativePackageShape(): void { - $this->nativeValidation = [ - [ - 'signatureValidation' => ['id' => 1, 'label' => 'Signature is valid.'], - 'certificateValidation' => ['id' => 3, 'label' => 'Certificate issuer is unknown.'], - ], + $this->nativeValidation[0]['signatureValidation'] = [ + 'id' => 1, + 'label' => 'Signature is valid.', + ]; + $this->nativeValidation[0]['certificateValidation'] = [ + 'id' => 3, + 'label' => 'Certificate issuer is unknown.', ]; $handler = $this->getHandler(); @@ -511,14 +521,24 @@ public function testGetCertificateChainUsesNativeValidationServiceForEachSignatu } public function testGetCertificateChainUsesNativeDigestMismatchValidation(): void { - $this->nativeValidation = [ - [ - 'signatureValidation' => [ - 'id' => 3, - 'label' => 'Digest mismatch.', - 'reason' => 'PDF content hash does not match signed digest', - ], - ], + $fixtureResource = fopen( + __DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', + 'r', + ); + $this->assertIsResource($fixtureResource); + + $service = new PdfSignatureValidationService( + $this->appConfig, + $this->l10n, + $this->logger, + ); + $this->nativeValidation = $service->validateFromResource($fixtureResource); + fclose($fixtureResource); + + $this->nativeValidation[0]['signatureValidation'] = [ + 'id' => 3, + 'label' => 'Digest mismatch.', + 'reason' => 'PDF content hash does not match signed digest', ]; $handler = $this->getHandler(); @@ -533,7 +553,7 @@ public function testGetCertificateChainUsesNativeDigestMismatchValidation(): voi $this->assertSame('Digest mismatch.', $result[0]['chain'][0]['signature_validation']['label']); } - public function testGetCertificateChainPropagatesUnexpectedNativeMetadataExtractionFailureAndResetsPolicyValidationContext(): void { + public function testGetCertificateChainPropagatesValidationFailureAndResetsPolicyValidationContext(): void { $this->logger->expects($this->never())->method('warning'); $policyCalls = []; @@ -547,31 +567,44 @@ 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 { - protected function extractNativeSignaturesFromContent(string $content): array { - throw new \RuntimeException('metadata boom'); - } - }; + $this->nativeValidationException = new \RuntimeException('validator boom'); + + $handler = new Pkcs12Handler( + $this->folderService, + $this->appConfig, + $certificateEngineFactory, + $this->l10n, + $this->footerHandler, + $this->logger, + $this->caIdentifierService, + $this->docMdpHandler, + $this->crlService, + $this->pdfSignatureValidationService, + ); $handler->setPolicyUserIdForValidation('requester'); - $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); + + $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; + $this->assertSame('validator boom', $exception->getMessage()); } 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'); + $reflection = new \ReflectionProperty( + Pkcs12Handler::class, + 'policyUserIdForValidation', + ); $this->assertNull($reflection->getValue($handler)); } From e683a6f40cfee25113f2e43ccae98a2cbf024881 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:23:11 -0300 Subject: [PATCH 10/76] test(validation): cover PDF modification warnings Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../validation/SignerDetails.spec.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/tests/components/validation/SignerDetails.spec.ts b/src/tests/components/validation/SignerDetails.spec.ts index 5eaf8018d4..61666da7a3 100644 --- a/src/tests/components/validation/SignerDetails.spec.ts +++ b/src/tests/components/validation/SignerDetails.spec.ts @@ -547,9 +547,22 @@ describe('SignerDetails.vue - Business Logic', () => { }) }) - describe('getSignatureCoverageMessage method', () => { - it('reports bytes outside the signed ByteRange', () => { - expect(wrapper.vm.getSignatureCoverageMessage()).toBe('The signature does not cover the entire document') + describe('document modification messages', () => { + it.each([ + ['unsigned_content', 'The document contains unsigned content after the latest signature'], + ['trailing_data', 'Unexpected data was found after the final PDF end marker'], + ['invalid_byte_range', 'The signature ByteRange is invalid'], + ['invalid_eof_boundary', 'The signed content does not end at a valid PDF end marker'], + ])('maps %s to the expected warning', (state, expected) => { + expect(wrapper.vm.getDocumentModificationMessage({ + document_modification_state: state, + })).toBe(expected) + }) + + it('does not warn when the document is unchanged', () => { + expect(wrapper.vm.hasDocumentModificationWarning({ + document_modification_state: 'unchanged', + })).toBe(false) }) }) From a33ed38c1c847fac95a2064ddf8109fc63237c16 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:23:31 -0300 Subject: [PATCH 11/76] chore(openapi): regenerate signer validation models Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- openapi-full.json | 10 ++++++++++ openapi.json | 10 ++++++++++ src/types/openapi/openapi-full.ts | 2 ++ src/types/openapi/openapi.ts | 2 ++ 4 files changed, 24 insertions(+) diff --git a/openapi-full.json b/openapi-full.json index 7b4bcac7c4..117d87f4f7 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -3003,6 +3003,16 @@ "covers_entire_document": { "type": "boolean" }, + "document_modification_state": { + "type": "string", + "enum": [ + "unchanged", + "unsigned_content", + "trailing_data", + "invalid_byte_range", + "invalid_eof_boundary" + ] + }, "me": { "type": "boolean" }, diff --git a/openapi.json b/openapi.json index c375c0d6bf..b33ed368f3 100644 --- a/openapi.json +++ b/openapi.json @@ -2376,6 +2376,16 @@ "covers_entire_document": { "type": "boolean" }, + "document_modification_state": { + "type": "string", + "enum": [ + "unchanged", + "unsigned_content", + "trailing_data", + "invalid_byte_range", + "invalid_eof_boundary" + ] + }, "me": { "type": "boolean" }, diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 41c88c65c2..ecccc11e36 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -1997,6 +1997,8 @@ export type components = { sign_request_uuid?: string; hash_algorithm?: string; covers_entire_document?: boolean; + /** @enum {string} */ + document_modification_state?: "unchanged" | "unsigned_content" | "trailing_data" | "invalid_byte_range" | "invalid_eof_boundary"; me: boolean; /** Format: int64 */ signingOrder?: number; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 7f3822c802..a5aa4b39d8 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -1555,6 +1555,8 @@ export type components = { sign_request_uuid?: string; hash_algorithm?: string; covers_entire_document?: boolean; + /** @enum {string} */ + document_modification_state?: "unchanged" | "unsigned_content" | "trailing_data" | "invalid_byte_range" | "invalid_eof_boundary"; me: boolean; /** Format: int64 */ signingOrder?: number; From 19bd2d62af94d5b1190fe858fb6270729385161b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:25:07 -0300 Subject: [PATCH 12/76] test(signing): expect package certificate validation state Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 3580951108..91457b23d0 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -517,7 +517,7 @@ public function testGetCertificateChainUsesNativeValidationServiceForEachSignatu $this->assertSame(1, $this->nativeValidationCalls); $this->assertNotEmpty($result); $this->assertSame(1, $result[0]['chain'][0]['signature_validation']['id']); - $this->assertSame(3, $result[0]['chain'][0]['certificate_validation']['id']); + $this->assertSame(2, $result[0]['chain'][0]['certificate_validation']['id']); } public function testGetCertificateChainUsesNativeDigestMismatchValidation(): void { From 5942a17c194fdb04ec0bfa4dd5f90408a8989e92 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:25:27 -0300 Subject: [PATCH 13/76] fix(validation): expose PDF modification helpers Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index d0856666b8..f7786e4810 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -612,7 +612,8 @@ defineExpose({ getValidityStatus, hasValidationStatus, getSignatureValidationMessage, - getSignatureCoverageMessage, + hasDocumentModificationWarning, + getDocumentModificationMessage, getCertificateTrustMessage, getValidityStatusAtSigning, getCrlValidationIconPath, From eeb2dfed106f6e1857975c3ccc0e8d1bb89b641b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:52:51 -0300 Subject: [PATCH 14/76] fix(validation): update PDF validator result types Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 70 +++++++++++++++---- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 51650461d5..16375a30dc 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -11,6 +11,7 @@ use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ExtractedSignature; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationReason; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationResult; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\ValidationState; @@ -75,7 +76,17 @@ public function setTrustedRoots(array $certificates): void { * Validate PDF signatures from file resource. * * @param resource $resource PDF file resource - * @return list + * @return list, + * timestamp: ?TimestampToken, + * signatureValidation: array, + * certificateValidation: array, + * raw: array{ + * signature: ValidationResult, + * certificate: ValidationResult, + * }, + * }> */ public function validateFromResource($resource): array { try { @@ -91,7 +102,17 @@ public function validateFromResource($resource): array { * Validate PDF signatures from binary content. * * @param string $pdfContent Binary PDF content - * @return list + * @return list, + * timestamp: ?TimestampToken, + * signatureValidation: array, + * certificateValidation: array, + * raw: array{ + * signature: ValidationResult, + * certificate: ValidationResult, + * }, + * }> */ public function validateFromString(string $pdfContent): array { try { @@ -105,14 +126,26 @@ public function validateFromString(string $pdfContent): array { /** * @param resource $resource - * @return list, certificateValidation: ValidationResult}> + * @return list, + * certificateValidation: ValidationResult, + * timestamp: ?TimestampToken, + * }> */ protected function validateNativeFromResource($resource): array { return $this->validator->validateFromResource($resource); } /** - * @return list, certificateValidation: ValidationResult}> + * @return list, + * certificateValidation: ValidationResult, + * timestamp: ?TimestampToken, + * }> */ protected function validateNativeFromString(string $pdfContent): array { return $this->validator->validateFromString($pdfContent); @@ -121,8 +154,24 @@ protected function validateNativeFromString(string $pdfContent): array { /** * Map validation results from PdfSignatureValidator to LibreSign format. * - * @param list $results Results from PdfSignatureValidator - * @return list + * @param list, + * certificateValidation: ValidationResult, + * timestamp: ?TimestampToken, + * }> $results Results from PdfSignatureValidator + * @return list, + * timestamp: ?TimestampToken, + * signatureValidation: array, + * certificateValidation: array, + * raw: array{ + * signature: ValidationResult, + * certificate: ValidationResult, + * }, + * }> */ private function mapValidationResults(array $results): array { $mapped = []; @@ -140,15 +189,10 @@ private function mapValidationResults(array $results): array { continue; } - $certificates = $result['certificates'] ?? []; - if (!is_array($certificates)) { - $certificates = []; - } - $mapped[] = [ 'signature' => $signature, - 'certificates' => array_values($certificates), - 'timestamp' => $result['timestamp'] ?? null, + 'certificates' => $result['certificates'], + 'timestamp' => $result['timestamp'], 'signatureValidation' => $this->mapSignatureValidation($sigValidation), 'certificateValidation' => $this->mapCertificateValidation($certValidation), 'raw' => [ From f56c6f77f6e6d07c973930a35f6ee92ecabc3589 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:53:00 -0300 Subject: [PATCH 15/76] fix(validation): preserve PDF modification state types Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/components/validation/SignerDetails.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/components/validation/SignerDetails.spec.ts b/src/tests/components/validation/SignerDetails.spec.ts index 61666da7a3..90c0bc664a 100644 --- a/src/tests/components/validation/SignerDetails.spec.ts +++ b/src/tests/components/validation/SignerDetails.spec.ts @@ -553,7 +553,7 @@ describe('SignerDetails.vue - Business Logic', () => { ['trailing_data', 'Unexpected data was found after the final PDF end marker'], ['invalid_byte_range', 'The signature ByteRange is invalid'], ['invalid_eof_boundary', 'The signed content does not end at a valid PDF end marker'], - ])('maps %s to the expected warning', (state, expected) => { + ] as const)('maps %s to the expected warning', (state, expected) => { expect(wrapper.vm.getDocumentModificationMessage({ document_modification_state: state, })).toBe(expected) From 633d5f72b31a929e670d7d43af04da6ac291cebc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:09:46 -0300 Subject: [PATCH 16/76] fix(validation): preserve TSA certificate hints Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index f34efb11da..f4411cab9d 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -209,16 +209,19 @@ private function mapTimestampToken(TimestampToken $timestamp): array { 'genTime' => $timestamp->generatedAt, 'policy' => $timestamp->policyOid, 'serialNumber' => $timestamp->serialNumber, + 'cnHints' => $timestamp->certificateSubject, ]; - $commonName = $timestamp->certificateSubject['CN'] ?? null; + $commonName = $timestamp->certificateSubject['commonName'] ?? null; if (is_string($commonName) && $commonName !== '') { $result['tsaName'] = $commonName; } return array_filter( $result, - static fn (mixed $value): bool => $value !== null && $value !== '', + static fn (mixed $value): bool => $value !== null + && $value !== '' + && $value !== [], ); } From 44dae65f13f0f4440f80f558af3beb83acca3fd2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:11:29 -0300 Subject: [PATCH 17/76] fix(validation): isolate scoped vendor result types Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 16375a30dc..315349a47f 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -135,7 +135,18 @@ public function validateFromString(string $pdfContent): array { * }> */ protected function validateNativeFromResource($resource): array { - return $this->validator->validateFromResource($resource); + /** @psalm-suppress UndefinedDocblockClass Vendor PHPDoc is not rewritten by PHP-Scoper. */ + $results = $this->validator->validateFromResource($resource); + + /** @var list, + * certificateValidation: ValidationResult, + * timestamp: ?TimestampToken, + * }> $results + */ + return $results; } /** @@ -148,7 +159,18 @@ protected function validateNativeFromResource($resource): array { * }> */ protected function validateNativeFromString(string $pdfContent): array { - return $this->validator->validateFromString($pdfContent); + /** @psalm-suppress UndefinedDocblockClass Vendor PHPDoc is not rewritten by PHP-Scoper. */ + $results = $this->validator->validateFromString($pdfContent); + + /** @var list, + * certificateValidation: ValidationResult, + * timestamp: ?TimestampToken, + * }> $results + */ + return $results; } /** From 39d488877eb7e6d476590089db4d8b46a5ac59b4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:26:33 -0300 Subject: [PATCH 18/76] chore: update pdf-signature-validator to 0.5.1 Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- 3rdparty | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty b/3rdparty index c5f453b5e3..4615d2be85 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit c5f453b5e3d514ff7b84daca9332a7a31e0bcfa1 +Subproject commit 4615d2be85290f4b051acc64d5f487179a3e656d From 74b28238ec2210dba387f4045e1647161a14553f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:26:58 -0300 Subject: [PATCH 19/76] refactor(validation): use validator result types directly Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../PdfSignatureValidationService.php | 26 ++----------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php index 315349a47f..16375a30dc 100644 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ b/lib/Service/Signature/PdfSignatureValidationService.php @@ -135,18 +135,7 @@ public function validateFromString(string $pdfContent): array { * }> */ protected function validateNativeFromResource($resource): array { - /** @psalm-suppress UndefinedDocblockClass Vendor PHPDoc is not rewritten by PHP-Scoper. */ - $results = $this->validator->validateFromResource($resource); - - /** @var list, - * certificateValidation: ValidationResult, - * timestamp: ?TimestampToken, - * }> $results - */ - return $results; + return $this->validator->validateFromResource($resource); } /** @@ -159,18 +148,7 @@ protected function validateNativeFromResource($resource): array { * }> */ protected function validateNativeFromString(string $pdfContent): array { - /** @psalm-suppress UndefinedDocblockClass Vendor PHPDoc is not rewritten by PHP-Scoper. */ - $results = $this->validator->validateFromString($pdfContent); - - /** @var list, - * certificateValidation: ValidationResult, - * timestamp: ?TimestampToken, - * }> $results - */ - return $results; + return $this->validator->validateFromString($pdfContent); } /** From 9273563aba6c2190c7ad466b8bd2c35ed4c1e491 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:29:26 -0300 Subject: [PATCH 20/76] test(validation): preserve TSA certificate hints Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Handler/SignEngine/Pkcs12HandlerTest.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 91457b23d0..602a192108 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -20,6 +20,7 @@ use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; +use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Model\TimestampToken; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAppConfig; @@ -506,6 +507,57 @@ public function testGetCertificateChainProvidesNativePackageShape(): void { $this->assertIsBool($leaf['covers_entire_document']); } + public function testGetCertificateChainMapsNativeTimestampData(): void { + $fixtureResource = fopen( + __DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', + 'r', + ); + $this->assertIsResource($fixtureResource); + + $service = new PdfSignatureValidationService( + $this->appConfig, + $this->l10n, + $this->logger, + ); + $this->nativeValidation = $service->validateFromResource($fixtureResource); + fclose($fixtureResource); + + $this->assertNotEmpty($this->nativeValidation); + + $generatedAt = new \DateTimeImmutable('2026-09-04T12:00:00+00:00'); + $this->nativeValidation[0]['timestamp'] = new TimestampToken( + $generatedAt, + '1.2.3.4', + '123456', + [ + 'commonName' => 'LibreSign Local TSA', + 'organizationName' => 'LibreCode', + ], + ); + + $resource = fopen( + __DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', + 'r', + ); + $this->assertIsResource($resource); + + $result = $this->getHandler()->getCertificateChain($resource); + fclose($resource); + + $this->assertNotEmpty($result); + $this->assertArrayHasKey('timestamp', $result[0]); + + $timestamp = $result[0]['timestamp']; + $this->assertSame($generatedAt, $timestamp['genTime']); + $this->assertSame('1.2.3.4', $timestamp['policy']); + $this->assertSame('123456', $timestamp['serialNumber']); + $this->assertSame('LibreSign Local TSA', $timestamp['tsaName']); + $this->assertSame([ + 'commonName' => 'LibreSign Local TSA', + 'organizationName' => 'LibreCode', + ], $timestamp['cnHints']); + } + public function testGetCertificateChainUsesNativeValidationServiceForEachSignature(): void { $handler = $this->getHandler(); $resource = fopen(__DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf', 'r'); From b55d2d079a6ad35dffba2c11efa17a439ed82c0c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:40:01 -0300 Subject: [PATCH 21/76] test(validation): cover complete validator results Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 6 +--- .../PdfSignatureValidationServiceTest.php | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index f4411cab9d..b0f1534970 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -210,13 +210,9 @@ private function mapTimestampToken(TimestampToken $timestamp): array { 'policy' => $timestamp->policyOid, 'serialNumber' => $timestamp->serialNumber, 'cnHints' => $timestamp->certificateSubject, + 'tsaName' => $timestamp->certificateSubject['commonName'] ?? null, ]; - $commonName = $timestamp->certificateSubject['commonName'] ?? null; - if (is_string($commonName) && $commonName !== '') { - $result['tsaName'] = $commonName; - } - return array_filter( $result, static fn (mixed $value): bool => $value !== null diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index d3ec4f0bf7..c4e299ced0 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -102,6 +102,36 @@ public function testMapReasonKeepsUnknownReasonUntouched(): void { $this->assertSame('custom runtime detail', $result['reason']); } + public function testValidateFromStringPreservesAllValidationResults(): void { + $pdfContent = file_get_contents( + __DIR__ . '/../../../fixtures/pdfs/small_valid-signed.pdf' + ); + $this->assertIsString($pdfContent); + + $service = new class($this->appConfig, $this->l10n, $this->logger) extends PdfSignatureValidationService { + protected function validateNativeFromString(string $pdfContent): array { + $results = parent::validateNativeFromString($pdfContent); + $this->assertNativeResultAvailable($results); + + return [$results[0], $results[0]]; + } + + private function assertNativeResultAvailable(array $results): void { + if ($results === []) { + throw new \RuntimeException('Expected signed PDF fixture.'); + } + } + }; + + $result = $service->validateFromString($pdfContent); + + $this->assertCount(2, $result); + $this->assertSame( + $result[0]['signature'], + $result[1]['signature'], + ); + } + public function testValidateFromStringReturnsEmptyListForUnsignedPdfException(): void { $this->logger->expects($this->never())->method('warning'); From bd7e236becf9ab732ce5cf25d852ecb3e6bc0099 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:40:16 -0300 Subject: [PATCH 22/76] test(validation): cover certificate display name priority Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../CertificateSignersMergeServiceTest.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php index 4121b3d022..b5ffabd589 100644 --- a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php +++ b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php @@ -154,6 +154,36 @@ public static function providerCertificateInfoMatching(): array { ]; } + public function testMergePrefersCertificateNameOverSubjectCommonName(): void { + $fileData = new \stdClass(); + $fileData->signers = []; + + $certData = [[ + 'uid' => 'email:signer@example.com', + 'chain' => [[ + 'name' => 'Certificate Name', + 'subject' => [ + 'CN' => 'Subject Common Name', + ], + ]], + ]]; + + $this->getService()->merge( + $fileData, + $certData, + 'example.com', + 'Signed', + fn (array $cert, string $host): ?string => null, + fn (string $method, string $value): string => $method . ':' . $value, + fn (string $accountId): ?string => null, + ); + + $this->assertSame( + 'Certificate Name', + $fileData->signers[0]->chain[0]['displayName'], + ); + } + public function testMergePromotesLeafCertValidityDatesToSignerRoot(): void { $fileData = new \stdClass(); $fileData->signers = []; From 2585b68b0871366d492c36168efae43d2cfbc9ee Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:04:50 -0300 Subject: [PATCH 23/76] test(validation): cover remaining mutation cases Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Handler/SignEngine/Pkcs12Handler.php | 7 ++- .../CertificateSignersMergeServiceTest.php | 42 ++++++++++++++ .../PdfSignatureValidationServiceTest.php | 56 +++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index b0f1534970..96d3676680 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -307,13 +307,16 @@ private function getRootCertificatePem(): string { return $this->rootCertificatePem; } $configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path'); + $caPemPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem'; + if (empty($configPath) || !is_dir($configPath) - || !is_readable($configPath . DIRECTORY_SEPARATOR . 'ca.pem') + || !is_readable($caPemPath) ) { return ''; } - $rootCertificatePem = file_get_contents($configPath . DIRECTORY_SEPARATOR . 'ca.pem'); + + $rootCertificatePem = file_get_contents($caPemPath); if ($rootCertificatePem === false) { return ''; } diff --git a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php index b5ffabd589..f158c031b2 100644 --- a/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php +++ b/tests/php/Unit/Service/File/CertificateSignersMergeServiceTest.php @@ -184,6 +184,48 @@ public function testMergePrefersCertificateNameOverSubjectCommonName(): void { ); } + public function testSingleContractSignerRequiresNumericSignRequestId(): void { + $service = $this->getService(); + $method = new \ReflectionMethod( + CertificateSignersMergeService::class, + 'getSingleContractSignerIndex', + ); + + $signers = [ + (object)[ + 'signRequestId' => 42, + ], + (object)[ + 'signRequestId' => 'not-numeric', + ], + ]; + + $this->assertSame( + 0, + $method->invoke($service, $signers), + ); + } + + public function testCertificateRootCaFlagIsPromotedToSigner(): void { + $service = $this->getService(); + $method = new \ReflectionMethod( + CertificateSignersMergeService::class, + 'enrichSignerWithCertificateValidation', + ); + + $signer = new \stdClass(); + + $method->invoke( + $service, + $signer, + [ + 'isLibreSignRootCA' => true, + ], + ); + + $this->assertTrue($signer->isLibreSignRootCA); + } + public function testMergePromotesLeafCertValidityDatesToSignerRoot(): void { $fileData = new \stdClass(); $fileData->signers = []; diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php index c4e299ced0..65aeeac047 100644 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php @@ -8,6 +8,7 @@ namespace OCA\Libresign\Tests\Unit\Service\Signature; +use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Service\Signature\PdfSignatureValidationService; use OCA\Libresign\Tests\Unit\TestCase; use OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Exception\UnsignedPdfException; @@ -132,6 +133,61 @@ private function assertNativeResultAvailable(array $results): void { ); } + public function testLoadsLibreSignCaCertificateFromConfiguredDirectory(): void { + $configPath = sys_get_temp_dir() + . DIRECTORY_SEPARATOR + . 'libresign-validator-ca-' + . bin2hex(random_bytes(8)); + + mkdir($configPath, 0700, true); + + $certificate = 'TEST_CA_CERTIFICATE'; + $caPemPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem'; + file_put_contents($caPemPath, $certificate); + + $appConfig = $this->createMock(IAppConfig::class); + $appConfig + ->method('getValueString') + ->willReturnCallback( + static function ( + string $appId, + string $key, + string $default = '', + ) use ($configPath): string { + if ($appId !== Application::APP_ID) { + return $default; + } + + return match ($key) { + 'config_path' => $configPath, + 'libresign_ca_certificate' => '', + default => $default, + }; + }, + ); + + try { + $service = new PdfSignatureValidationService( + $appConfig, + $this->l10n, + $this->logger, + ); + + $property = new \ReflectionProperty( + PdfSignatureValidationService::class, + 'libresignCaCertificate', + ); + + $this->assertSame( + $certificate, + $property->getValue($service), + ); + } finally { + @unlink($caPemPath); + @rmdir($configPath); + } + } + public function testValidateFromStringReturnsEmptyListForUnsignedPdfException(): void { $this->logger->expects($this->never())->method('warning'); From 9bee55dab5b0cf0bbd58ced7406e30150e62d391 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:40:15 -0300 Subject: [PATCH 24/76] fix(validation): wrap long detail text Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 51 ++++++++++++++------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index f7786e4810..7ffbe6e5a4 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -561,19 +561,27 @@ function hasDocMdpInfo(signer: SignerModel) { function getModificationStatusIcon(signer: SignerModel) { if (!signer.modification_validation) { return undefined } + const status = signer.modification_validation.status - if (status === MODIFICATION_UNMODIFIED || status === MODIFICATION_ALLOWED) { + if (status === MODIFICATION_UNMODIFIED) { return mdiCheckCircle } - return mdiAlertCircle + if (status === MODIFICATION_ALLOWED) { + return mdiAlertCircle + } + return mdiCancel } function getModificationStatusClass(signer: SignerModel) { if (!signer.modification_validation) { return '' } + const status = signer.modification_validation.status - if (status === MODIFICATION_UNMODIFIED || status === MODIFICATION_ALLOWED) { + if (status === MODIFICATION_UNMODIFIED) { return 'icon-success' } + if (status === MODIFICATION_ALLOWED) { + return 'icon-warning' + } return 'icon-error' } @@ -629,30 +637,41 @@ defineExpose({ From fa8f7e9ceb2d3ac3888ce82d974e8319db9644b8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:43:41 -0300 Subject: [PATCH 56/76] fix(validation): use plain glyphs for signer status badges Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 57 +++++++++++++-------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index 91d6abc006..5aeb624868 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -257,21 +257,7 @@ import { computed, ref } from 'vue' import CertificateChain from './CertificateChain.vue' import SignerTimestamp from './SignerTimestamp.vue' -import { - mdiAlertCircle, - mdiAlertCircleOutline, - mdiCancel, - mdiCheckCircle, - mdiCloseCircle, - mdiHelpCircle, - mdiInformationOutline, - mdiKey, - mdiShieldAlert, - mdiShieldCheck, - mdiShieldOff, - mdiUnfoldLessHorizontal, - mdiUnfoldMoreHorizontal, -} from '@mdi/js' +import { mdiAlertCircleOutline, mdiHelpCircle, mdiInformationOutline, mdiKey, mdiShieldAlert, mdiShieldCheck, mdiShieldOff, mdiUnfoldLessHorizontal, mdiUnfoldMoreHorizontal, mdiCheck, mdiAlert, mdiClose } from '@mdi/js' type ValidationState = { @@ -433,14 +419,14 @@ function getIconValidityPath(signer: SignerModel) { } if (signer.modification_validation?.status === MODIFICATION_VIOLATION || isRevokedBeforeSigning(signer)) { - return mdiCloseCircle + return mdiClose } if (hasDocumentModificationWarning(signer) || (signer.certificate_validation !== undefined && signer.certificate_validation.id !== 1)) { - return mdiAlertCircle + return mdiAlert } - return mdiCheckCircle + return mdiCheck } function getSignerValidationClass(signer: SignerModel) { @@ -592,12 +578,12 @@ function getModificationStatusIcon(signer: SignerModel) { const status = signer.modification_validation.status if (status === MODIFICATION_UNMODIFIED) { - return mdiCheckCircle + return mdiCheck } if (status === MODIFICATION_ALLOWED) { - return mdiAlertCircle + return mdiAlert } - return mdiCancel + return mdiClose } function getModificationStatusClass(signer: SignerModel) { @@ -696,12 +682,39 @@ defineExpose({ line-height: 1.4; } .signer-validation-icon { - display: flex; + display: inline-flex; align-items: center; justify-content: center; width: 44px; + min-width: 44px; height: 44px; + min-height: 44px; flex: 0 0 44px; + border-radius: 50%; + box-sizing: border-box; + background-color: transparent; + color: inherit; +} + +.signer-validation-icon :deep(.material-design-icon), +.signer-validation-icon :deep(svg) { + color: inherit !important; + fill: currentColor !important; +} + +.icon-success.signer-validation-icon { + background-color: rgba(var(--color-success-rgb), 0.18); + color: var(--color-success); +} + +.icon-warning.signer-validation-icon { + background-color: rgba(var(--color-warning-rgb), 0.18); + color: var(--color-warning); +} + +.icon-error.signer-validation-icon { + background-color: rgba(var(--color-error-rgb), 0.16); + color: var(--color-error); } .icon-success { From dbecdbb26f557ea1352846bc665df881544c9021 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:48:00 -0300 Subject: [PATCH 57/76] fix(validation): restore required status icons Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/components/validation/SignerDetails.vue | 32 ++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/validation/SignerDetails.vue b/src/components/validation/SignerDetails.vue index 5aeb624868..a625164946 100644 --- a/src/components/validation/SignerDetails.vue +++ b/src/components/validation/SignerDetails.vue @@ -39,7 +39,7 @@