diff --git a/3rdparty b/3rdparty index d326a4844b..7146e7625b 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit d326a4844be1adc93b39e6afd6810657e5d114e5 +Subproject commit 7146e7625b7ea982fbbba82d1c07d21c3915f09f diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index f7b2776f32..5bb90a1242 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -8,6 +8,7 @@ namespace OCA\Libresign\Handler\SignEngine; +use DateTime; use OCA\Libresign\AppInfo\Application; use OCA\Libresign\Exception\LibresignException; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; @@ -18,21 +19,17 @@ 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\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 OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; 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 string $rootCertificatePem = ''; private bool $isLibreSignFile = false; @@ -43,12 +40,11 @@ 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); } @@ -96,26 +92,13 @@ public function setIsLibreSignFile(): void { #[\Override] public function getCertificateChain($resource): array { $certificates = []; - $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, - $metadata, - $validation - ); + $result = $this->processSignature($resource, $signature); if (empty($result['chain'])) { continue; @@ -123,18 +106,14 @@ public function getCertificateChain($resource): array { $certificates[] = $result; } - return $certificates; } - private function processSignature($resource, ?string $signature, array $metadata = [], array $validation = []): array { + private function processSignature($resource, ?string $signature): array { $result = []; if (!$signature) { - $result['chain'][0]['signature_validation'] = [ - 'id' => 3, - 'label' => $this->l10n->t('Digest mismatch.'), - ]; + $result['chain'][0]['signature_validation'] = $this->getReadableSigState('Digest Mismatch.'); return $result; } @@ -144,7 +123,7 @@ private function processSignature($resource, ?string $signature, array $metadata $chain = $this->extractCertificateChain($signature); if (!empty($chain)) { $result['chain'] = $this->orderCertificates($chain); - $result = $this->enrichLeafWithNativeData($result, $metadata, $validation); + $result = $this->enrichLeafWithPopplerData($resource, $result); } $result = $this->extractDocMdpData($resource, $result); @@ -296,89 +275,216 @@ private function getRootCertificatePem(): string { return $this->rootCertificatePem; } - private function enrichLeafWithNativeData(array $result, array $metadata, array $validation): array { + private function enrichLeafWithPopplerData($resource, array $result): array { if (empty($result['chain'])) { return $result; } - $leaf = &$result['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]; - } + $popplerOnlyFields = ['field', 'range', 'certificate_validation']; + if (!isset($result['chain'][0]['subject'])) { + return $result; } - - if (isset($validation['signatureValidation']) && is_array($validation['signatureValidation'])) { - $signatureValidation = $validation['signatureValidation']; - - // Keep legacy OpenSSL result when native validator reports this known false-positive. - if (!$this->isDigestMismatchSignatureValidation($validation)) { - $leaf['signature_validation'] = $signatureValidation; + $needPoppler = false; + foreach ($popplerOnlyFields as $field) { + if (empty($result['chain'][0][$field])) { + $needPoppler = true; + break; } } - - if (isset($validation['certificateValidation']) && is_array($validation['certificateValidation'])) { - $leaf['certificate_validation'] = $validation['certificateValidation']; + if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) { + $needPoppler = true; } - - if (!isset($leaf['certificate_validation'])) { - $leaf['certificate_validation'] = [ - 'id' => 3, - 'label' => $this->l10n->t('Certificate issuer is unknown.'), - ]; + 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; } - /** - * 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 $validation): bool { - $rawSignatureValidation = $validation['raw']['signature'] ?? null; - if ($rawSignatureValidation instanceof ValidationResult) { - return $rawSignatureValidation->reasonCode === ValidationReason::DIGEST_MISMATCH - || $rawSignatureValidation->state === ValidationState::DIGEST_MISMATCH; + 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]; + } } - - $signatureValidation = $validation['signatureValidation'] ?? null; - return is_array($signatureValidation) && ($signatureValidation['id'] ?? null) === 3; + return []; } - /** - * @param resource $resource - * @return array - */ - private function extractNativeSignatureMetadata($resource): array { + private function popplerUtilsPdfSignFallback($resource): array { + if (!empty($this->signaturesFromPoppler)) { + return $this->signaturesFromPoppler; + } + if (shell_exec('which pdfsig') === null) { + return $this->signaturesFromPoppler; + } rewind($resource); $content = stream_get_contents($resource); - if (!is_string($content) || $content === '') { - return []; - } + $tempFile = $this->tempManager->getTemporaryFile('file.pdf'); + file_put_contents($tempFile, $content); - try { - $signatures = $this->extractNativeSignaturesFromContent($content); - } catch (UnsignedPdfException) { - return []; + $content = shell_exec('env TZ=UTC pdfsig ' . $tempFile); + if (empty($content)) { + return $this->signaturesFromPoppler; } - $metadata = []; + $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; + } - 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, - ]; + $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; + } + } } + return $this->signaturesFromPoppler; + } - return $metadata; + private function getReadableSigState(string $status) { + return match ($status) { + 'Signature is Valid.' => [ + 'id' => 1, + 'label' => $this->l10n->t('Signature is valid.'), + ], + 'Signature is Invalid.' => [ + 'id' => 2, + 'label' => $this->l10n->t('Signature is invalid.'), + ], + 'Digest Mismatch.' => [ + '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.' => [ + 'id' => 5, + 'label' => $this->l10n->t('Signature has not yet been verified.'), + ], + default => [ + 'id' => 6, + 'label' => $this->l10n->t('Unknown validation failure.'), + ], + }; } - protected function extractNativeSignaturesFromContent(string $content): array { - return $this->pdfSignatureExtractor->extractFromString($content); + private function getReadableCertState(string $status) { + return match ($status) { + 'Certificate is Trusted.' => [ + 'id' => 1, + 'label' => $this->l10n->t('Certificate is trusted.'), + ], + "Certificate issuer isn't Trusted." => [ + 'id' => 2, + 'label' => $this->l10n->t("Certificate issuer isn't trusted."), + ], + 'Certificate issuer is unknown.' => [ + 'id' => 3, + 'label' => $this->l10n->t('Certificate issuer is unknown.'), + ], + 'Certificate has been Revoked.' => [ + 'id' => 4, + 'label' => $this->l10n->t('Certificate has been revoked.'), + ], + 'Certificate has Expired' => [ + 'id' => 5, + 'label' => $this->l10n->t('Certificate has expired'), + ], + 'Certificate has not yet been verified.' => [ + '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.') + ], + }; + } + + private function parseDistinguishedNameWithMultipleValues(string $dn): array { + $result = []; + $pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn); + + 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; + } + } + + return $result; } private function der2pem($derData) { diff --git a/lib/Service/Signature/PdfSignatureValidationService.php b/lib/Service/Signature/PdfSignatureValidationService.php deleted file mode 100644 index 2a222deb4c..0000000000 --- a/lib/Service/Signature/PdfSignatureValidationService.php +++ /dev/null @@ -1,314 +0,0 @@ -validator = new PdfSignatureValidator(); - $this->loadLibreSignCaCertificate(); - } - - private function loadLibreSignCaCertificate(): void { - $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; - } - } - } - - $alternateConfig = $this->appConfig->getValueString( - Application::APP_ID, - 'libresign_ca_certificate' - ); - if (!empty($alternateConfig)) { - $this->libresignCaCertificate = $alternateConfig; - $this->validator->addTrustedRoot($alternateConfig); - } - } - - public function addTrustedRoot(string $certificatePem): void { - $this->validator->addTrustedRoot($certificatePem); - } - - public function setTrustedRoots(array $certificates): void { - $this->validator->setTrustedRoots($certificates); - } - - /** - * Validate PDF signatures from file resource. - * - * @param resource $resource PDF file resource - * @return list - */ - public function validateFromResource($resource): array { - try { - $results = $this->validateNativeFromResource($resource); - } catch (UnsignedPdfException) { - return []; - } - - return $this->mapValidationResults($results); - } - - /** - * Validate PDF signatures from binary content. - * - * @param string $pdfContent Binary PDF content - * @return list - */ - public function validateFromString(string $pdfContent): array { - try { - $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); - } - - /** - * Map validation results from PdfSignatureValidator to LibreSign format. - * - * @param list $results Results from PdfSignatureValidator - * @return list - */ - private function mapValidationResults(array $results): 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; - } - - 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, - ], - }; - } - - 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, - ], - }; - } - - 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) { - // 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]] - ); - } - - $prefix = 'Certificate validation failed: '; - 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]); - } - - 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, - }; - } - - public function isLibreSignCaLoaded(): bool { - return !empty($this->libresignCaCertificate); - } - - 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 84e10a288d..888ec3f955 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -18,13 +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 OCA\Libresign\Vendor\LibreSign\PdfSignatureValidator\Parser\PdfSignatureExtractor; 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; @@ -36,33 +35,26 @@ 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 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('isSetupOk')->willReturn(true); - $this->certificateEngine->method('validateRootCertificate')->willReturnCallback(static function (): void { - }); $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(); $this->certificateEngineFactory->method('getEngine') ->willReturn($this->certificateEngine); @@ -79,12 +71,11 @@ 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(); @@ -95,12 +86,11 @@ 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, ); } @@ -450,28 +440,4 @@ private function normalizeDistinguishedName(mixed $distinguishedName): array { return $distinguishedName; } - 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']); - } - } diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index f8b1e837e3..e2a27beb66 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -21,10 +21,9 @@ 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 OCA\Libresign\Vendor\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; @@ -40,12 +39,11 @@ 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); @@ -54,13 +52,12 @@ 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(); } @@ -83,12 +80,11 @@ 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(); diff --git a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php b/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php deleted file mode 100644 index 137e1e1e9e..0000000000 --- a/tests/php/Unit/Service/Signature/PdfSignatureValidationServiceTest.php +++ /dev/null @@ -1,133 +0,0 @@ -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 { - $service = $this->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']); - } - - 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']); - } - - 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 */ - $service = $reflection->newInstanceWithoutConstructor(); - - $l10nProperty = $reflection->getProperty('l10n'); - $l10nProperty->setValue($service, $this->l10n); - - return $service; - } - - /** - * @return array - */ - private function invokePrivateMethod(PdfSignatureValidationService $service, string $method, ValidationResult $result): array { - $reflection = new \ReflectionClass($service); - $target = $reflection->getMethod($method); - /** @var array $mapped */ - $mapped = $target->invoke($service, $result); - return $mapped; - } -}