diff --git a/REUSE.toml b/REUSE.toml index 4e7bea0c17..0b6ae4a1eb 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -50,9 +50,9 @@ path = [ "src/types/openapi/openapi.ts", "tests/php/Unit/Handler/mock/cert.json", "tests/php/fixtures/cfssl/newcert-with-success.json", - "tests/php/fixtures/real_jsignpdf_level1.pdf", - "tests/php/fixtures/small_valid-signed.pdf", - "tests/php/fixtures/small_valid.pdf", + "tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf", + "tests/php/fixtures/pdfs/small_valid-signed.pdf", + "tests/php/fixtures/pdfs/small_valid.pdf", "tests/integration/composer.json", "tests/integration/composer.lock", "tests/integration/features/**/*.feature", diff --git a/composer.json b/composer.json index 31daf4dd3c..be06320b0d 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,9 @@ }, "autoload-dev": { "psr-4": { - "OCP\\": "vendor/nextcloud/ocp/OCP" + "OCP\\": "vendor/nextcloud/ocp/OCP", + "OCA\\Libresign\\Tests\\Unit\\": "tests/php/Unit/", + "OCA\\Libresign\\Tests\\Fixtures\\": "tests/php/fixtures/" } }, "require": { diff --git a/lib/Controller/DevelopController.php b/lib/Controller/DevelopController.php index b42f6b9fca..b724144b8f 100644 --- a/lib/Controller/DevelopController.php +++ b/lib/Controller/DevelopController.php @@ -48,7 +48,7 @@ public function pdf(): FileDisplayResponse|Response { if (!$this->isDebugMode()) { return new DataResponse([], Http::STATUS_NOT_FOUND); } - $file = new InMemoryFile('file.pdf', file_get_contents(__DIR__ . '/../../tests/php/fixtures/small_valid.pdf')); + $file = new InMemoryFile('file.pdf', file_get_contents(__DIR__ . '/../../tests/php/fixtures/pdfs/small_valid.pdf')); $response = new FileDisplayResponse($file); $response->setHeaders([ 'Content-Disposition' => 'inline; filename="file.pdf"', diff --git a/lib/Handler/SignEngine/Pkcs12Handler.php b/lib/Handler/SignEngine/Pkcs12Handler.php index 11c1a3aaf3..d5040814ec 100644 --- a/lib/Handler/SignEngine/Pkcs12Handler.php +++ b/lib/Handler/SignEngine/Pkcs12Handler.php @@ -52,32 +52,23 @@ public function __construct( private function getSignatures($resource): iterable { rewind($resource); $content = stream_get_contents($resource); - preg_match_all( - '/ByteRange\s*\[\s*(?\d+)\s+(?\d+)\s+(?\d+)\s+(?\d+)\s*\]/', - $content, - $bytes - ); - if (empty($bytes['offset1']) || empty($bytes['length1']) || empty($bytes['offset2']) || empty($bytes['length2'])) { - throw new LibresignException($this->l10n->t('Unsigned file.')); - } - for ($i = 0; $i < count($bytes['offset1']); $i++) { - $offset1 = (int)$bytes['offset1'][$i]; - $length1 = (int)$bytes['length1'][$i]; - $offset2 = (int)$bytes['offset2'][$i]; + preg_match_all('/\/Contents\s*<([0-9a-fA-F]+)>/', $content, $contents, PREG_OFFSET_CAPTURE); - $signatureStart = $offset1 + $length1 + 1; - $signatureLength = $offset2 - $signatureStart - 1; + if (empty($contents[1])) { + throw new LibresignException($this->l10n->t('Unsigned file.')); + } - rewind($resource); + $seenHexSignatures = []; + foreach ($contents[1] as $match) { + $signatureHex = $match[0]; - $signature = stream_get_contents($resource, $signatureLength, $signatureStart); - if ($signature === false) { - yield null; + if (isset($seenHexSignatures[$signatureHex])) { continue; } + $seenHexSignatures[$signatureHex] = true; - $decodedSignature = @hex2bin($signature); + $decodedSignature = @hex2bin($signatureHex); if ($decodedSignature === false) { yield null; continue; @@ -101,7 +92,17 @@ public function getCertificateChain($resource): array { $certificates = []; foreach ($this->getSignatures($resource) as $signature) { - $certificates[] = $this->processSignature($resource, $signature); + if (!$signature) { + continue; + } + + $result = $this->processSignature($resource, $signature); + + if (empty($result['chain'])) { + continue; + } + + $certificates[] = $result; } return $certificates; } diff --git a/tests/php/Api/Controller/FileControllerTest.php b/tests/php/Api/Controller/FileControllerTest.php index 14ea4586a3..60bf194463 100644 --- a/tests/php/Api/Controller/FileControllerTest.php +++ b/tests/php/Api/Controller/FileControllerTest.php @@ -55,7 +55,7 @@ public function testValidateWithSuccessUsingUnloggedUser():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -93,7 +93,7 @@ public function testValidateWithSuccessUsingSigner():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -149,7 +149,7 @@ public function testSendNewFile():void { ->withMethod('POST') ->withRequestBody([ 'name' => 'test', - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], ]); $this->assertRequest(); diff --git a/tests/php/Api/Controller/FileElementControllerTest.php b/tests/php/Api/Controller/FileElementControllerTest.php index de986eaa41..3271533024 100644 --- a/tests/php/Api/Controller/FileElementControllerTest.php +++ b/tests/php/Api/Controller/FileElementControllerTest.php @@ -23,7 +23,7 @@ public function testPostSuccess():array { $user = $this->createAccount('username', 'password'); $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/IdDocsControllerTest.php b/tests/php/Api/Controller/IdDocsControllerTest.php index dd7496c979..53e803d03b 100644 --- a/tests/php/Api/Controller/IdDocsControllerTest.php +++ b/tests/php/Api/Controller/IdDocsControllerTest.php @@ -60,7 +60,7 @@ public function testPostIdDocsWithSuccess():void { [ 'type' => 'IDENTIFICATION', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ] ] ] diff --git a/tests/php/Api/Controller/NotifyControllerTest.php b/tests/php/Api/Controller/NotifyControllerTest.php index 36344efce1..e80a569173 100644 --- a/tests/php/Api/Controller/NotifyControllerTest.php +++ b/tests/php/Api/Controller/NotifyControllerTest.php @@ -49,7 +49,7 @@ public function testNotifySignersWithSuccess():void { $appConfig->setValueArray(Application::APP_ID, 'groups_request_sign', ['admin','testGroup']); $appConfig->setValueBool(Application::APP_ID, 'notifyUnsignedUser', true); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/RequestSignatureControllerTest.php b/tests/php/Api/Controller/RequestSignatureControllerTest.php index 9b09c3b9e0..6df036702c 100644 --- a/tests/php/Api/Controller/RequestSignatureControllerTest.php +++ b/tests/php/Api/Controller/RequestSignatureControllerTest.php @@ -61,7 +61,7 @@ public function testPostRegisterWithSuccess():void { ->withRequestBody([ 'name' => 'filename', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ], 'users' => [ [ @@ -114,7 +114,7 @@ public function testPatchRegisterWithSuccess():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Api/Controller/SignFileControllerTest.php b/tests/php/Api/Controller/SignFileControllerTest.php index be9b93eee6..da88383965 100644 --- a/tests/php/Api/Controller/SignFileControllerTest.php +++ b/tests/php/Api/Controller/SignFileControllerTest.php @@ -78,7 +78,7 @@ public function testSignUsingFileIdWithAlreadySignedFile():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -122,7 +122,7 @@ public function testSignUsingFileIdWithNotFoundFile():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -166,7 +166,7 @@ public function testSignUsingUuidWithEmptyToken():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -203,7 +203,7 @@ public function testSignWithCertificateButEmptyPassword():void { $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -259,7 +259,7 @@ public function testAccountSignatureEndpointWithFailure():void { public function testDeleteSignFileIdSignRequestIdWithSuccess():void { $user = $this->createAccount('allowrequestsign', 'password', 'testGroup'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ @@ -307,7 +307,7 @@ public function testDeleteSignFileIdSignRequestIdWithError():void { public function testDeleteUsingSignFileIdWithSuccess():void { $user = $this->createAccount('allowrequestsign', 'password', 'testGroup'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php b/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php index 7130340b98..aa0e0ba5ef 100644 --- a/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php +++ b/tests/php/Unit/Controller/AEnvironmentPageAwareControllerTest.php @@ -71,7 +71,7 @@ public function testLoadFileUuidWhenFileNotFound(): void { $user = $this->createAccount('username', 'password'); $user->setEMailAddress('person@test.coop'); $file = $this->requestSignFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ [ diff --git a/tests/php/Unit/Handler/DocMdpHandlerTest.php b/tests/php/Unit/Handler/DocMdpHandlerTest.php index 6fc69294ea..9c87f7f47f 100644 --- a/tests/php/Unit/Handler/DocMdpHandlerTest.php +++ b/tests/php/Unit/Handler/DocMdpHandlerTest.php @@ -12,14 +12,13 @@ use OCA\Libresign\Db\File; use OCA\Libresign\Enum\DocMdpLevel; use OCA\Libresign\Handler\DocMdpHandler; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\IL10N; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; final class DocMdpHandlerTest extends TestCase { - use PdfFixtureTrait; private IL10N&MockObject $l10n; private DocMdpHandler $handler; @@ -30,9 +29,9 @@ protected function setUp(): void { } public function testUnsignedPdfIsDetectedAsLevelNone(): void { - $pdfContent = $this->createMinimalPdf(); + $pdfContent = PdfGenerator::createMinimalPdf(); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -40,9 +39,9 @@ public function testUnsignedPdfIsDetectedAsLevelNone(): void { } public function testP0AllowsAnyModification(): void { - $pdfContent = $this->createPdfWithDocMdp(0, withModifications: true); + $pdfContent = PdfGenerator::createPdfWithDocMdp(0, withModifications: true); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -51,9 +50,9 @@ public function testP0AllowsAnyModification(): void { } public function testP1ProhibitsAnyModification(): void { - $pdfContent = $this->createPdfWithDocMdp(1, withModifications: true); + $pdfContent = PdfGenerator::createPdfWithDocMdp(1, withModifications: true); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -62,9 +61,9 @@ public function testP1ProhibitsAnyModification(): void { } public function testP2AllowsFormFieldModifications(): void { - $pdfContent = $this->createPdfWithFormFieldModification(2); + $pdfContent = PdfGenerator::createPdfWithFormFieldModification(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -73,9 +72,9 @@ public function testP2AllowsFormFieldModifications(): void { } public function testP2ProhibitsAnnotationModifications(): void { - $pdfContent = $this->createPdfWithAnnotationModification(2); + $pdfContent = PdfGenerator::createPdfWithAnnotationModification(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -84,9 +83,9 @@ public function testP2ProhibitsAnnotationModifications(): void { } public function testP3AllowsFormFieldModifications(): void { - $pdfContent = $this->createPdfWithFormFieldModification(3); + $pdfContent = PdfGenerator::createPdfWithFormFieldModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -95,9 +94,9 @@ public function testP3AllowsFormFieldModifications(): void { } public function testP3AllowsAnnotationModifications(): void { - $pdfContent = $this->createPdfWithAnnotationModification(3); + $pdfContent = PdfGenerator::createPdfWithAnnotationModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -106,9 +105,9 @@ public function testP3AllowsAnnotationModifications(): void { } public function testP3ProhibitsStructuralModifications(): void { - $pdfContent = $this->createPdfWithStructuralModification(3); + $pdfContent = PdfGenerator::createPdfWithStructuralModification(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -117,9 +116,9 @@ public function testP3ProhibitsStructuralModifications(): void { } public function testP2AllowsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(2); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -128,9 +127,9 @@ public function testP2AllowsSubsequentSignatures(): void { } public function testP3AllowsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(3); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -139,9 +138,9 @@ public function testP3AllowsSubsequentSignatures(): void { } public function testP1ProhibitsSubsequentSignatures(): void { - $pdfContent = $this->createPdfWithSubsequentSignature(1); + $pdfContent = PdfGenerator::createPdfWithSubsequentSignature(1); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -150,9 +149,9 @@ public function testP1ProhibitsSubsequentSignatures(): void { } public function testExtractsDocMdpFromSignatureReferenceNotPerms(): void { - $pdfContent = $this->createPdfWithDocMdpInSignatureReference(2); + $pdfContent = PdfGenerator::createPdfWithDocMdpInSignatureReference(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -160,9 +159,9 @@ public function testExtractsDocMdpFromSignatureReferenceNotPerms(): void { } public function testExtractsDocMdpFromFirstCertifyingSignature(): void { - $pdfContent = $this->createPdfWithApprovalThenCertifyingSignature(); + $pdfContent = PdfGenerator::createPdfWithApprovalThenCertifyingSignature(); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -170,9 +169,9 @@ public function testExtractsDocMdpFromFirstCertifyingSignature(): void { } public function testP2AllowsPageTemplateInstantiation(): void { - $pdfContent = $this->createPdfWithPageTemplate(2); + $pdfContent = PdfGenerator::createPdfWithPageTemplate(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -181,9 +180,9 @@ public function testP2AllowsPageTemplateInstantiation(): void { } public function testP3AllowsPageTemplateInstantiation(): void { - $pdfContent = $this->createPdfWithPageTemplate(3); + $pdfContent = PdfGenerator::createPdfWithPageTemplate(3); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -192,9 +191,9 @@ public function testP3AllowsPageTemplateInstantiation(): void { } public function testExtractsDocMdpWithIndirectReferenceItiStyle(): void { - $pdfContent = $this->createPdfWithIndirectReferencesItiStyle(2); + $pdfContent = PdfGenerator::createPdfWithIndirectReferencesItiStyle(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -202,9 +201,9 @@ public function testExtractsDocMdpWithIndirectReferenceItiStyle(): void { } public function testValidatesTransformParamsVersion(): void { - $pdfContent = $this->createPdfWithDocMdpVersion12(2); + $pdfContent = PdfGenerator::createPdfWithDocMdp(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -212,9 +211,9 @@ public function testValidatesTransformParamsVersion(): void { } public function testRejectsDocMdpWithoutVersion(): void { - $pdfContent = $this->createPdfWithDocMdpWithoutVersion(2); + $pdfContent = PdfGenerator::createPdfWithDocMdpWithoutVersion(2); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -222,9 +221,9 @@ public function testRejectsDocMdpWithoutVersion(): void { } public function testRejectsDocMdpWithInvalidVersion(): void { - $pdfContent = $this->createPdfWithDocMdpInvalidVersion(2, '1.0'); + $pdfContent = PdfGenerator::createPdfWithDocMdpInvalidVersion(2, '1.0'); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -232,9 +231,9 @@ public function testRejectsDocMdpWithInvalidVersion(): void { } public function testRejectsDocMdpWithInvalidVersionIndirectRef(): void { - $pdfContent = $this->createPdfWithIndirectReferencesInvalidVersion(2, '1.3'); + $pdfContent = PdfGenerator::createPdfWithIndirectReferencesInvalidVersion(2, '1.3'); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -252,9 +251,9 @@ public static function docMdpLevelExtractionProvider(): array { #[DataProvider('docMdpLevelExtractionProvider')] public function testExtractsDocMdpPermissionLevel(int $pValue, DocMdpLevel $expectedLevel): void { - $pdfContent = $this->createPdfWithDocMdp($pValue); + $pdfContent = PdfGenerator::createPdfWithDocMdp($pValue); - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -266,9 +265,9 @@ public function testExtractsDocMdpPermissionLevel(int $pValue, DocMdpLevel $expe // ISO 32000-1 Table 252 validation tests public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { - $pdf = $this->createPdfWithInvalidSignatureType(); + $pdf = PdfGenerator::createPdfWithInvalidSignatureType(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -276,9 +275,9 @@ public function testRejectsSignatureDictionaryWithoutTypeWhenPresent(): void { } public function testRejectsSignatureWithoutFilterEntry(): void { - $pdf = $this->createPdfWithoutFilterEntry(); + $pdf = PdfGenerator::createPdfWithoutFilterEntry(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -286,9 +285,9 @@ public function testRejectsSignatureWithoutFilterEntry(): void { } public function testRejectsSignatureWithoutByteRange(): void { - $pdf = $this->createPdfWithoutByteRange(); + $pdf = PdfGenerator::createPdfWithoutByteRange(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -296,9 +295,9 @@ public function testRejectsSignatureWithoutByteRange(): void { } public function testRejectsMultipleDocMdpSignatures(): void { - $pdf = $this->createPdfWithMultipleDocMdpSignatures(); + $pdf = PdfGenerator::createPdfWithMultipleDocMdpSignatures(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -306,9 +305,9 @@ public function testRejectsMultipleDocMdpSignatures(): void { } public function testRejectsDocMdpNotFirstSignature(): void { - $pdf = $this->createPdfWithDocMdpNotFirst(); + $pdf = PdfGenerator::createPdfWithDocMdpNotFirst(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -316,9 +315,9 @@ public function testRejectsDocMdpNotFirstSignature(): void { } public function testRejectsSigRefWithoutTransformMethod(): void { - $pdf = $this->createPdfWithSigRefWithoutTransformMethod(); + $pdf = PdfGenerator::createPdfWithSigRefWithoutTransformMethod(); - $resource = $this->createResourceFromContent($pdf); + $resource = PdfGenerator::createResourceFromContent($pdf); $result = $this->handler->extractDocMdpData($resource); fclose($resource); @@ -348,13 +347,13 @@ public static function additionalSignaturesProvider(): array { public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bool $withModifications, bool $expectedAllowed): void { if ($level === 'unsigned') { // PDF without any signature (virgin PDF) - $pdfContent = $this->createMinimalPdf(); + $pdfContent = PdfGenerator::createMinimalPdf(); } else { // PDF with DocMDP signature at specified level (0, 1, 2, or 3) - $pdfContent = $this->createPdfWithDocMdp($level, $withModifications); + $pdfContent = PdfGenerator::createPdfWithDocMdp($level, $withModifications); } - $resource = $this->createResourceFromContent($pdfContent); + $resource = PdfGenerator::createResourceFromContent($pdfContent); $result = $this->handler->allowsAdditionalSignatures($resource); fclose($resource); @@ -362,14 +361,14 @@ public function testAdditionalSignaturesBasedOnDocMdpLevel(string|int $level, bo } public function testRealJSignPdfWithDocMdpLevel1(): void { - $pdfPath = __DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf'; + $pdfPath = __DIR__ . '/../../fixtures/pdfs/real_jsignpdf_level1.pdf'; if (!file_exists($pdfPath)) { $this->markTestSkipped('Real JSignPdf test PDF not found'); } $content = file_get_contents($pdfPath); - $resource = $this->createResourceFromContent($content); + $resource = PdfGenerator::createResourceFromContent($content); $data = $this->handler->extractDocMdpData($resource); diff --git a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php index 2fd1b34e5a..3fd013e10a 100644 --- a/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php +++ b/tests/php/Unit/Handler/SignEngine/Pkcs12HandlerTest.php @@ -13,6 +13,7 @@ use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Service\CaIdentifierService; use OCA\Libresign\Service\FolderService; +use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAppConfig; @@ -236,22 +237,16 @@ public function testGetCertificateChainWithCorruptedSignature(): void { $corruptedPdf = "%PDF-1.4\n" . "1 0 obj<>endobj\n" - . "ZZZZZZZZ\n" // Invalid hex data - will cause hex2bin to fail + . "ZZZZZZZZ\n" // Invalid hex data - will cause extraction to fail . str_repeat('x', 100); $resource = fopen('php://memory', 'r+'); fwrite($resource, $corruptedPdf); rewind($resource); - $result = $handler->getCertificateChain($resource); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - $this->assertArrayHasKey('chain', $result[0]); - $this->assertArrayHasKey('signature_validation', $result[0]['chain'][0]); - $this->assertIsArray($result[0]['chain'][0]['signature_validation']); - $this->assertEquals(3, $result[0]['chain'][0]['signature_validation']['id']); // Digest Mismatch - $this->assertStringContainsString('Digest mismatch', $result[0]['chain'][0]['signature_validation']['label']); + $this->expectException(\OCA\Libresign\Exception\LibresignException::class); + $this->expectExceptionMessage('Unsigned file'); + $handler->getCertificateChain($resource); fclose($resource); } @@ -329,4 +324,52 @@ public function testIntegrationWithFileSystem(): void { $this->expectException(\OCA\Libresign\Exception\LibresignException::class); $handler->getPfxOfCurrentSigner('test_user'); } + + public function testGetCertificateChainWithAllFixtures(): void { + $handler = $this->getHandler(); + $catalog = new PdfFixtureCatalog(); + + foreach ($catalog->getAll() as $fixture) { + if (!$fixture->shouldExtract()) { + continue; + } + + $resource = $fixture->openResource(); + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertCount($fixture->getSignatureCount(), $result, $fixture->getFilename()); + + foreach ($result as $signatureData) { + $this->assertArrayHasKey('chain', $signatureData); + $this->assertIsArray($signatureData['chain']); + } + } + } + + public function testDocMdpPdfsExtraction(): void { + $handler = $this->getHandler(); + $catalog = new PdfFixtureCatalog(); + $docmdpFixtures = $catalog->getWithDocMdp(); + + $this->assertGreaterThan(0, count($docmdpFixtures)); + + foreach ($docmdpFixtures as $fixture) { + if (!$fixture->shouldExtract()) { + continue; + } + + $resource = $fixture->openResource(); + $result = $handler->getCertificateChain($resource); + fclose($resource); + + $this->assertCount($fixture->getSignatureCount(), $result, $fixture->getFilename()); + + foreach ($result as $signatureData) { + $this->assertArrayHasKey('chain', $signatureData); + $this->assertGreaterThan(0, count($signatureData['chain'])); + } + } + } + } diff --git a/tests/php/Unit/Helper/ValidateHelperTest.php b/tests/php/Unit/Helper/ValidateHelperTest.php index a19caba178..3c6b89da33 100644 --- a/tests/php/Unit/Helper/ValidateHelperTest.php +++ b/tests/php/Unit/Helper/ValidateHelperTest.php @@ -271,22 +271,22 @@ public static function dataValidateBase64(): array { false ], [ - base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, true ], [ - 'data:application/pdf;base63,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/pdf;base63,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, false ], [ - 'data:application/bla;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/bla;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, false ], [ - 'data:application/pdf;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')), + 'data:application/pdf;base64,' . base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')), ValidateHelper::TYPE_TO_SIGN, true ], @@ -475,7 +475,7 @@ public function testValidateVisibleElementsWithSuccess():void { $elements = [[ 'type' => 'signature', 'file' => [ - 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf')) + 'base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf')) ] ]]; $actual = $this->getValidateHelper()->validateVisibleElements($elements, ValidateHelper::TYPE_TO_SIGN); diff --git a/tests/php/Unit/Service/FileServiceTest.php b/tests/php/Unit/Service/FileServiceTest.php index f78263f396..ab1bdfe347 100644 --- a/tests/php/Unit/Service/FileServiceTest.php +++ b/tests/php/Unit/Service/FileServiceTest.php @@ -35,6 +35,7 @@ function is_uploaded_file($filename) { use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\IdentifyMethodService; use OCA\Libresign\Service\PdfParserService; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCP\Accounts\IAccountManager; use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; @@ -54,7 +55,6 @@ function is_uploaded_file($filename) { * @internal */ final class FileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { - use \OCA\Libresign\Tests\Unit\PdfFixtureTrait; protected FileMapper $fileMapper; protected SignRequestMapper $signRequestMapper; protected FileElementMapper $fileElementMapper; @@ -254,7 +254,7 @@ function (self $self, FileService $service): void { $self->markTestSkipped('Skipping test for not signed file due to environment limitations with PHP >= 8.4.'); } $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -265,8 +265,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -281,7 +281,7 @@ function (self $self, FileService $service): void { $self->markTestSkipped('Skipping test for not signed file due to environment limitations with PHP >= 8.4.'); } $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -292,8 +292,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid-signed.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -310,7 +310,7 @@ function (self $self, FileService $service): void { $self->userManager->method('get')->willReturn(null); $self->userManager->method('getByEmail')->willReturn([]); $notSigned = tempnam(sys_get_temp_dir(), 'not_signed'); - copy(realpath(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), $notSigned); + copy(realpath(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), $notSigned); $service ->setFileFromRequest([ 'tmp_name' => $notSigned, @@ -322,8 +322,8 @@ function (self $self, FileService $service): void { }, [ 'status' => File::STATUS_NOT_LIBRESIGN_FILE, - 'size' => filesize(__DIR__ . '/../../fixtures/small_valid-signed.pdf'), - 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/small_valid-signed.pdf'), + 'size' => filesize(__DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), + 'hash' => hash_file('sha256', __DIR__ . '/../../fixtures/pdfs/small_valid-signed.pdf'), 'pdfVersion' => '1.6', 'totalPages' => 1, 'name' => 'small_valid.pdf', @@ -464,7 +464,7 @@ function (self $self, FileService $service): void { } public function testValidateFileContentRejectsDocMdpLevel1(): void { - $pdfContent = $this->createPdfWithDocMdpLevel1(); + $pdfContent = PdfGenerator::createCompletePdfStructure(1); $service = $this->getService(); $this->expectException(\OCA\Libresign\Exception\LibresignException::class); @@ -474,7 +474,7 @@ public function testValidateFileContentRejectsDocMdpLevel1(): void { public function testValidateFileContentAllowsDocMdpLevel2(): void { $this->expectNotToPerformAssertions(); - $pdfContent = $this->createPdfWithDocMdpLevel2(); + $pdfContent = PdfGenerator::createCompletePdfStructure(2); $service = $this->getService(); $service->validateFileContent($pdfContent, 'pdf'); @@ -482,7 +482,7 @@ public function testValidateFileContentAllowsDocMdpLevel2(): void { public function testValidateFileContentAllowsDocMdpLevel3(): void { $this->expectNotToPerformAssertions(); - $pdfContent = $this->createPdfWithDocMdp(3); + $pdfContent = PdfGenerator::createCompletePdfStructure(3); $service = $this->getService(); $service->validateFileContent($pdfContent, 'pdf'); @@ -490,7 +490,7 @@ public function testValidateFileContentAllowsDocMdpLevel3(): void { public function testValidateFileContentAllowsUnsignedPdf(): void { $this->expectNotToPerformAssertions(); - $pdfPath = __DIR__ . '/../../fixtures/small_valid.pdf'; + $pdfPath = __DIR__ . '/../../fixtures/pdfs/small_valid.pdf'; $pdfContent = file_get_contents($pdfPath); $service = $this->getService(); diff --git a/tests/php/Unit/Service/PdfParseServiceTest.php b/tests/php/Unit/Service/PdfParseServiceTest.php index 9e159011a8..9dc8dc10c0 100644 --- a/tests/php/Unit/Service/PdfParseServiceTest.php +++ b/tests/php/Unit/Service/PdfParseServiceTest.php @@ -90,7 +90,7 @@ public static function providerGetMetadataWithSuccess(): array { return [ [ 'disablePdfInfo' => true, - 'tests/php/fixtures/small_valid.pdf', + 'tests/php/fixtures/pdfs/small_valid.pdf', [ 'p' => 1, 'd' => [ @@ -100,7 +100,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => true, - 'tests/php/fixtures/small_valid-signed.pdf', + 'tests/php/fixtures/pdfs/small_valid-signed.pdf', [ 'p' => 1, 'd' => [ @@ -110,7 +110,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => false, - 'tests/php/fixtures/small_valid.pdf', + 'tests/php/fixtures/pdfs/small_valid.pdf', [ 'p' => 1, 'd' => [ @@ -120,7 +120,7 @@ public static function providerGetMetadataWithSuccess(): array { ], [ 'disablePdfInfo' => false, - 'tests/php/fixtures/small_valid-signed.pdf', + 'tests/php/fixtures/pdfs/small_valid-signed.pdf', [ 'p' => 1, 'd' => [ diff --git a/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php b/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php index ecb1652256..8296a46b62 100644 --- a/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php +++ b/tests/php/Unit/Service/PdfSignatureDetectionServiceTest.php @@ -10,13 +10,13 @@ use OCA\Libresign\Handler\SignEngine\SignEngineFactory; use OCA\Libresign\Service\PdfSignatureDetectionService; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; +use OCA\Libresign\Tests\Fixtures\PdfFixtureCatalog; +use OCA\Libresign\Tests\Fixtures\PdfGenerator; use OCA\Libresign\Tests\Unit\TestCase; use PHPUnit\Framework\Attributes\DataProvider; use Psr\Log\LoggerInterface; class PdfSignatureDetectionServiceTest extends TestCase { - use PdfFixtureTrait; private PdfSignatureDetectionService $service; @@ -33,15 +33,22 @@ public function setUp(): void { } public static function pdfContentProvider(): array { - $fixture = new class { - use PdfFixtureTrait; - }; + $catalog = new PdfFixtureCatalog(); + + $signedFixture = $catalog->getByFilename('small_valid-signed.pdf'); + $signedPdf = $signedFixture ? file_get_contents($signedFixture->getFilePath()) : ''; + + $unsignedFixture = $catalog->getByFilename('small_valid.pdf'); + $unsignedPdf = $unsignedFixture ? file_get_contents($unsignedFixture->getFilePath()) : ''; + return [ - 'signed PDF with DocMDP level 1' => [fn () => $fixture->createPdfWithDocMdp(1), true], - 'signed PDF with DocMDP level 2' => [fn () => $fixture->createPdfWithDocMdp(2), true], - 'signed PDF with DocMDP level 3' => [fn () => $fixture->createPdfWithDocMdp(3), true], - 'unsigned minimal PDF' => [fn () => $fixture->createMinimalPdf(), false], + 'signed PDF from catalog' => [fn () => $signedPdf, true], + 'unsigned PDF from catalog' => [fn () => $unsignedPdf, false], + 'synthetic PDF with DocMDP level 1' => [fn () => PdfGenerator::createPdfWithDocMdp(1), false], + 'synthetic PDF with DocMDP level 2' => [fn () => PdfGenerator::createPdfWithDocMdp(2), false], + 'synthetic PDF with DocMDP level 3' => [fn () => PdfGenerator::createPdfWithDocMdp(3), false], + 'synthetic minimal PDF unsigned' => [fn () => PdfGenerator::createMinimalPdf(), false], 'empty string' => [fn () => '', false], 'invalid content' => [fn () => 'not a valid pdf content', false], ]; diff --git a/tests/php/Unit/Service/RequestSignatureServiceTest.php b/tests/php/Unit/Service/RequestSignatureServiceTest.php index 414927c50b..e634f03286 100644 --- a/tests/php/Unit/Service/RequestSignatureServiceTest.php +++ b/tests/php/Unit/Service/RequestSignatureServiceTest.php @@ -151,7 +151,7 @@ public function testValidateEmptyUsersCollection():void { $this->expectExceptionMessage('Empty users list'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'userManager' => $this->user ]); @@ -161,7 +161,7 @@ public function testValidateUserCollectionNotArray():void { $this->expectExceptionMessage('User list needs to be an array'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => 'asdfg', 'userManager' => $this->user @@ -172,7 +172,7 @@ public function testValidateUserEmptyCollection():void { $this->expectExceptionMessage('Empty users list'); $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => null, 'userManager' => $this->user @@ -181,7 +181,7 @@ public function testValidateUserEmptyCollection():void { public function testValidateSuccess():void { $actual = $this->getService()->validateNewRequestToFile([ - 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/small_valid.pdf'))], + 'file' => ['base64' => base64_encode(file_get_contents(__DIR__ . '/../../fixtures/pdfs/small_valid.pdf'))], 'name' => 'test', 'users' => [ ['identify' => ['email' => 'jhondoe@test.coop']] diff --git a/tests/php/Unit/Service/SignFileServiceTest.php b/tests/php/Unit/Service/SignFileServiceTest.php index 415f2a397a..ef6a4b0801 100644 --- a/tests/php/Unit/Service/SignFileServiceTest.php +++ b/tests/php/Unit/Service/SignFileServiceTest.php @@ -36,7 +36,6 @@ use OCA\Libresign\Service\PdfSignatureDetectionService; use OCA\Libresign\Service\SignerElementsService; use OCA\Libresign\Service\SignFileService; -use OCA\Libresign\Tests\Unit\PdfFixtureTrait; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -60,7 +59,6 @@ * @group DB */ final class SignFileServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { - use PdfFixtureTrait; private IL10N&MockObject $l10n; private FooterHandler&MockObject $footerHandler; private FileMapper&MockObject $fileMapper; @@ -627,7 +625,6 @@ public function testGetSignatureParamsCommonName( $service->setSignRequest($signRequest); $actual = $this->invokePrivate($service, 'getSignatureParams'); - $this->assertEquals($expectedIssuerCN, $actual['IssuerCommonName']); $this->assertEquals($expectedSignerCN, $actual['SignerCommonName']); $this->assertEquals('uuid', $actual['DocumentUUID']); @@ -1240,7 +1237,7 @@ public function testSignThrowsExceptionWhenDocMdpLevel1Detected(): void { $service = $this->getService(['getNextcloudFile', 'getEngine']); $nextcloudFile = $this->createMock(\OCP\Files\File::class); - $nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/real_jsignpdf_level1.pdf')); + $nextcloudFile->method('getContent')->willReturn(file_get_contents(__DIR__ . '/../../fixtures/pdfs/real_jsignpdf_level1.pdf')); $service->method('getNextcloudFile')->willReturn($nextcloudFile); $engineMock = $this->createMock(Pkcs12Handler::class); @@ -1296,27 +1293,27 @@ public function testValidateDocMdpAllowsSignaturesWithVariousPdfFixtures( public static function provideValidateDocMdpAllowsSignaturesScenarios(): array { return [ 'Unsigned PDF - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createMinimalPdf(), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createMinimalPdf(), 'shouldThrowException' => false, ], 'DocMDP level 0 (not certified) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(0, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(0, false), 'shouldThrowException' => false, ], 'DocMDP level 1 (no changes allowed) - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, false), 'shouldThrowException' => true, ], 'DocMDP level 2 (form filling allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(2, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(2, false), 'shouldThrowException' => false, ], 'DocMDP level 3 (annotations allowed) - should NOT throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(3, false), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(3, false), 'shouldThrowException' => false, ], 'DocMDP level 1 with modifications - SHOULD throw exception' => [ - 'pdfContentGenerator' => fn (self $test) => $test->createPdfWithDocMdp(1, true), + 'pdfContentGenerator' => fn (self $test) => \OCA\Libresign\Tests\Fixtures\PdfGenerator::createPdfWithDocMdp(1, true), 'shouldThrowException' => true, ], ]; diff --git a/tests/php/fixtures/PdfFixtureCatalog.php b/tests/php/fixtures/PdfFixtureCatalog.php new file mode 100644 index 0000000000..02467c9730 --- /dev/null +++ b/tests/php/fixtures/PdfFixtureCatalog.php @@ -0,0 +1,242 @@ +basePath = $catalogPath ?? __DIR__ . '/../fixtures/pdfs'; + $catalogFile = $this->basePath . '/catalog.yaml'; + + if (!file_exists($catalogFile)) { + throw new \RuntimeException("Catalog file not found: $catalogFile"); + } + + $this->catalog = Yaml::parseFile($catalogFile); + } + + /** @return PdfFixture[] */ + public function getAll(): array { + $fixtures = []; + foreach ($this->catalog['pdfs'] as $pdfData) { + $fixtures[] = new PdfFixture($this->basePath, $pdfData); + } + return $fixtures; + } + + /** + * @return PdfFixture[] + * @example getBy(['signature_count' => 1, 'tool' => 'libresign']) + */ + public function getBy(array $criteria): array { + return array_filter($this->getAll(), function (PdfFixture $fixture) use ($criteria) { + $metadata = $fixture->getMetadata(); + + foreach ($criteria as $key => $value) { + // Handle special filter keys + switch ($key) { + case 'has_docmdp': + if ($value !== $this->hasDocMdp($metadata)) { + return false; + } + break; + case 'has_tsa': + if ($value !== $this->hasTsa($metadata)) { + return false; + } + break; + case 'tool': + if (!$this->usesTool($metadata, $value)) { + return false; + } + break; + case 'min_signatures': + if ($metadata['signature_count'] < $value) { + return false; + } + break; + case 'is_libresign_ca': + if (!$this->hasLibresignCa($metadata)) { + return false; + } + break; + default: + // Direct property match + if (!isset($metadata[$key]) || $metadata[$key] !== $value) { + return false; + } + } + } + return true; + }); + } + + public function getByFilename(string $filename): ?PdfFixture { + foreach ($this->getAll() as $fixture) { + if ($fixture->getFilename() === $filename) { + return $fixture; + } + } + return null; + } + + /** @return PdfFixture[] */ + public function getMultiSignature(): array { + return $this->getBy(['min_signatures' => 2]); + } + + /** @return PdfFixture[] */ + public function getWithDocMdp(): array { + return $this->getBy(['has_docmdp' => true]); + } + + /** @return PdfFixture[] */ + public function getByTool(string $tool): array { + return $this->getBy(['tool' => $tool]); + } + + private function hasDocMdp(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return true; + } + } + return false; + } + + private function hasTsa(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['features']['tsa'] === true) { + return true; + } + } + return false; + } + + private function usesTool(array $metadata, string $tool): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['tool'] === $tool) { + return true; + } + } + return false; + } + + private function hasLibresignCa(array $metadata): bool { + foreach ($metadata['signatures'] as $sig) { + if ($sig['certificate']['is_libresign_ca'] === true) { + return true; + } + } + return false; + } +} + +class PdfFixture { + private string $basePath; + private array $metadata; + + public function __construct(string $basePath, array $metadata) { + $this->basePath = $basePath; + $this->metadata = $metadata; + } + + public function getFilename(): string { + return $this->metadata['filename']; + } + + public function getFilePath(): string { + return $this->basePath . '/' . $this->metadata['filename']; + } + + public function getDescription(): string { + return $this->metadata['description']; + } + + public function getSignatureCount(): int { + return $this->metadata['signature_count']; + } + + public function getSignatures(): array { + return $this->metadata['signatures']; + } + + public function getTestExpectations(): array { + return $this->metadata['test_expectations']; + } + + public function getMetadata(): array { + return $this->metadata; + } + + public function shouldExtract(): bool { + return $this->metadata['test_expectations']['should_extract']; + } + + public function shouldValidate(): bool { + return $this->metadata['test_expectations']['should_validate']; + } + + public function getExpectedModificationStatus(): ?int { + return $this->metadata['test_expectations']['expected_modifications']; + } + + /** @return resource */ + public function openResource() { + $path = $this->getFilePath(); + if (!file_exists($path)) { + throw new \RuntimeException("PDF fixture not found: $path"); + } + $resource = fopen($path, 'rb'); + if ($resource === false) { + throw new \RuntimeException("Failed to open PDF fixture: $path"); + } + return $resource; + } + + public function hasDocMdp(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return true; + } + } + return false; + } + + public function getDocMdpLevel(): ?int { + foreach ($this->metadata['signatures'] as $sig) { + if (!empty($sig['features']['docmdp'])) { + return $sig['features']['docmdp']; + } + } + return null; + } + + public function hasTsa(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if ($sig['features']['tsa'] === true) { + return true; + } + } + return false; + } + + public function hasLibresignCa(): bool { + foreach ($this->metadata['signatures'] as $sig) { + if ($sig['certificate']['is_libresign_ca'] === true) { + return true; + } + } + return false; + } +} diff --git a/tests/php/Unit/PdfFixtureTrait.php b/tests/php/fixtures/PdfGenerator.php similarity index 81% rename from tests/php/Unit/PdfFixtureTrait.php rename to tests/php/fixtures/PdfGenerator.php index 737a943732..afc856d5a4 100644 --- a/tests/php/Unit/PdfFixtureTrait.php +++ b/tests/php/fixtures/PdfGenerator.php @@ -1,55 +1,22 @@ >\nendobj\n" . "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" . "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" . "xref\n0 4\ntrailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n190\n%%EOF"; } - /** - * Create a complete PDF with DocMDP signature - * - * This creates a more complete PDF structure that passes FPDI validation - * and includes proper DocMDP transformation parameters. - * - * @param int $pValue DocMDP permission level (0=not certified, 1=no changes, 2=form filling, 3=form+annotations) - * @param bool $withModifications Whether to add modifications after signature - * @return string PDF content as string - */ - /** - * Create PDF with DocMDP signature - * - * Uses complete FPDI-valid structure for FileService tests, - * or minimal structure for DocMdpHandler tests. - */ - public function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { - // FileService needs FPDI-valid PDF (has validatePdfStringWithFpdi) - if (str_contains(static::class, 'FileServiceTest')) { - return $this->createCompletePdfStructure($pValue, $withModifications); - } - - // DocMdpHandler only needs minimal structure + public static function createPdfWithDocMdp(int $pValue, bool $withModifications = false): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -78,7 +45,6 @@ public function createPdfWithDocMdp(int $pValue, bool $withModifications = false while (strlen($pdf) < $targetLength) { $pdf .= ' '; } - $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; } else { @@ -89,16 +55,88 @@ public function createPdfWithDocMdp(int $pValue, bool $withModifications = false return $pdf; } + public static function createPdfWithFormFieldModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [9 0 R] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 9 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n9 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified) /Rect [100 100 200 120] >>\nendobj\n"; + $pdf .= "xref\n0 10\ntrailer\n<< /Size 10 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + public static function createPdfWithAnnotationModification(int $pValue): string { + $pdf = "%PDF-1.7\n"; + $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; + $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; + $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; + $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R] >>\nendobj\n"; + + $signatureStart = strlen($pdf) + 200; + $signatureLength = 100; + $offset1 = 0; + $length1 = $signatureStart; + $offset2 = $signatureStart + $signatureLength; + + $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; + $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; + $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; + $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; + $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; + + $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; + + $length2 = 200; + $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); + + $targetLength = $offset2 + $length2; + while (strlen($pdf) < $targetLength) { + $pdf .= ' '; + } + + $pdf .= "\n7 0 obj\n<< /Type /Annot /Subtype /Text /Rect [100 100 200 200] >>\nendobj\n"; + $pdf .= "xref\n7 1\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; + + return $pdf; + } + + public static function createResourceFromContent(string $content) { + $resource = tmpfile(); + fwrite($resource, $content); + rewind($resource); + return $resource; + } + /** * FPDI-compliant PDF structure (for FileService validation) - * - * FileService.validateFileContent() uses Smalot PDF parser which requires: - * - Valid xref table with correct offsets - * - Content streams - * - Font dictionaries - * - Proper trailer + * Creates a complete PDF with proper xref table and stream objects */ - private function createCompletePdfStructure(int $pValue, bool $withModifications): string { + public static function createCompletePdfStructure(int $pValue, bool $withModifications = false): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; @@ -157,52 +195,10 @@ private function createCompletePdfStructure(int $pValue, bool $withModifications return $pdf; } - protected function createPdfWithFormFieldModification(int $pValue): string { - $pdf = "%PDF-1.7\n"; - $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; - $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; - $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; - $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 7 0 R] >>\nendobj\n"; - - $signatureStart = strlen($pdf) + 200; - $signatureLength = 100; - $offset1 = 0; - $length1 = $signatureStart; - $offset2 = $signatureStart + $signatureLength; - - $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; - $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; - $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; - $pdf .= "/TransformParams << /Type /TransformParams /P $pValue /V /1.2 >> >> ]\n"; - $pdf .= '/Contents <' . str_repeat('00', $signatureLength / 2) . "> >>\nendobj\n"; - - $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; - - $length2 = 200; - $pdf = str_replace('PLACEHOLDER_LENGTH2', (string)$length2, $pdf); - - $targetLength = $offset2 + $length2; - while (strlen($pdf) < $targetLength) { - $pdf .= ' '; - } - - $pdf .= "\n7 0 obj\n<< /FT /Tx /T (TextField1) /V (Modified Value) >>\nendobj\n"; - $pdf .= "xref\n0 8\ntrailer\n<< /Size 8 /Root 1 0 R >>\nstartxref\n" . strlen($pdf) . "\n%%EOF"; - - return $pdf; - } - /** - * Create PDF with annotation modification + * DocMDP signature without /V entry (ISO 32000-1 violation) */ - protected function createPdfWithAnnotationModification(int $pValue): string { - return $this->createPdfWithDocMdp($pValue, withModifications: true); - } - - /** - * Create PDF with DocMDP but without version in TransformParams - */ - protected function createPdfWithDocMdpWithoutVersion(int $pValue): string { + public static function createPdfWithDocMdpWithoutVersion(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -215,7 +211,6 @@ protected function createPdfWithDocMdpWithoutVersion(int $pValue): string { $length1 = $signatureStart; $offset2 = $signatureStart + $signatureLength; - // Missing /V in TransformParams $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; $pdf .= "/ByteRange [ $offset1 $length1 $offset2 PLACEHOLDER_LENGTH2 ]\n"; $pdf .= "/Reference [ << /Type /SigRef /TransformMethod /DocMDP\n"; @@ -234,9 +229,9 @@ protected function createPdfWithDocMdpWithoutVersion(int $pValue): string { } /** - * Create PDF with DocMDP with invalid version + * DocMDP signature with non-standard version */ - protected function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { + public static function createPdfWithDocMdpInvalidVersion(int $pValue, string $version): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -267,45 +262,9 @@ protected function createPdfWithDocMdpInvalidVersion(int $pValue, string $versio } /** - * Create PDF with DocMDP version 1.2 (valid per ICP-Brasil) - */ - protected function createPdfWithDocMdpVersion12(int $pValue): string { - return $this->createPdfWithDocMdp($pValue); - } - - /** - * Convenience methods for specific DocMDP levels - */ - protected function createPdfWithDocMdpLevel0(): string { - return $this->createPdfWithDocMdp(0); - } - - protected function createPdfWithDocMdpLevel1(): string { - return $this->createPdfWithDocMdp(1); - } - - protected function createPdfWithDocMdpLevel2(): string { - return $this->createPdfWithDocMdp(2); - } - - protected function createPdfWithDocMdpLevel3(): string { - return $this->createPdfWithDocMdp(3); - } - - /** - * Create resource from PDF content (for DocMdpHandler tests) - */ - protected function createResourceFromContent(string $content) { - $resource = tmpfile(); - fwrite($resource, $content); - fseek($resource, 0); - return $resource; - } - - /** - * Create PDF with structural modification (adding a new page) + * PDF with page added after signing (tests P=1 rejection) */ - protected function createPdfWithStructuralModification(int $pValue): string { + public static function createPdfWithStructuralModification(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R 7 0 R] /Count 2 >>\nendobj\n"; @@ -341,9 +300,9 @@ protected function createPdfWithStructuralModification(int $pValue): string { } /** - * Create PDF with subsequent signature (multiple signatures) + * PDF with second signature added after DocMDP (tests ISO 32000-2 §12.8.2.3) */ - protected function createPdfWithSubsequentSignature(int $pValue): string { + public static function createPdfWithSubsequentSignature(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -381,9 +340,9 @@ protected function createPdfWithSubsequentSignature(int $pValue): string { } /** - * Create PDF with DocMDP in signature Reference (without /Perms) + * DocMDP in /Reference but not in /Perms (tests §12.8.2.2 validation) */ - protected function createPdfWithDocMdpInSignatureReference(int $pValue): string { + public static function createPdfWithDocMdpInSignatureReference(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -414,9 +373,9 @@ protected function createPdfWithDocMdpInSignatureReference(int $pValue): string } /** - * Create PDF with approval signature followed by certifying signature + * Approval signature, then certification (violates "DocMDP must be first") */ - protected function createPdfWithApprovalThenCertifyingSignature(): string { + public static function createPdfWithApprovalThenCertifyingSignature(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -451,9 +410,9 @@ protected function createPdfWithApprovalThenCertifyingSignature(): string { } /** - * Create PDF with page template (XObject Form) + * PDF with XObject form added after signing (tests P=3 acceptance) */ - protected function createPdfWithPageTemplate(int $pValue): string { + public static function createPdfWithPageTemplate(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /Perms << /DocMDP 5 0 R >> >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -489,9 +448,9 @@ protected function createPdfWithPageTemplate(int $pValue): string { } /** - * Create PDF with indirect references (ITI style) + * ITI's indirect reference style: /Reference [ 7 0 R ] instead of inline dict */ - protected function createPdfWithIndirectReferencesItiStyle(int $pValue): string { + public static function createPdfWithIndirectReferencesItiStyle(int $pValue): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -525,9 +484,9 @@ protected function createPdfWithIndirectReferencesItiStyle(int $pValue): string } /** - * Create PDF with indirect references and invalid version (for testing rejection) + * Indirect references with invalid /V value */ - protected function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { + public static function createPdfWithIndirectReferencesInvalidVersion(int $pValue, string $version): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -561,9 +520,9 @@ protected function createPdfWithIndirectReferencesInvalidVersion(int $pValue, st } /** - * ISO 32000-1 Table 252 validation: Signature dictionary with invalid /Type + * Signature with wrong /Type (not /Sig) */ - protected function createPdfWithInvalidSignatureType(): string { + public static function createPdfWithInvalidSignatureType(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -577,9 +536,9 @@ protected function createPdfWithInvalidSignatureType(): string { } /** - * ISO 32000-1 Table 252 validation: Signature dictionary without /Filter + * Signature without /Filter entry */ - protected function createPdfWithoutFilterEntry(): string { + public static function createPdfWithoutFilterEntry(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -593,9 +552,9 @@ protected function createPdfWithoutFilterEntry(): string { } /** - * ISO 32000-1: Signature without required /ByteRange + * Signature without /ByteRange */ - protected function createPdfWithoutByteRange(): string { + public static function createPdfWithoutByteRange(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; @@ -609,23 +568,21 @@ protected function createPdfWithoutByteRange(): string { } /** - * ISO 32000-1 12.8.2.2.1: Multiple DocMDP signatures (invalid) + * Two signatures both with DocMDP (forbidden per ISO 32000-2 §12.8.2.2) */ - protected function createPdfWithMultipleDocMdpSignatures(): string { + public static function createPdfWithMultipleDocMdpSignatures(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - // First DocMDP signature $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; $pdf .= "/ByteRange [0 100 200 100]\n"; $pdf .= "/Reference [7 0 R] >>\nendobj\n"; $pdf .= "6 0 obj\n<< /FT /Sig /T (Signature1) /V 5 0 R >>\nendobj\n"; $pdf .= "7 0 obj\n<< /Type /SigRef /TransformMethod /DocMDP /TransformParams << /Type /TransformParams /P 2 /V /1.2 >> >>\nendobj\n"; - // Second DocMDP signature (INVALID per ISO) $pdf .= "8 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; $pdf .= "/ByteRange [0 100 300 100]\n"; $pdf .= "/Reference [9 0 R] >>\nendobj\n"; @@ -637,21 +594,19 @@ protected function createPdfWithMultipleDocMdpSignatures(): string { } /** - * ISO 32000-1 12.8.2.2.1: DocMDP not as first signature (invalid) + * DocMDP not on first signature (violates first-signature-only rule) */ - protected function createPdfWithDocMdpNotFirst(): string { + public static function createPdfWithDocMdpNotFirst(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; $pdf .= "3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n"; $pdf .= "4 0 obj\n<< /SigFlags 3 /Fields [6 0 R 10 0 R] >>\nendobj\n"; - // First signature: regular approval signature (no DocMDP) $pdf .= "5 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; $pdf .= "/ByteRange [0 100 200 100] >>\nendobj\n"; $pdf .= "6 0 obj\n<< /FT /Sig /T (ApprovalSignature) /V 5 0 R >>\nendobj\n"; - // Second signature: DocMDP certification (INVALID - must be first) $pdf .= "7 0 obj\n<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /ETSI.CAdES.detached\n"; $pdf .= "/ByteRange [0 100 300 100]\n"; $pdf .= "/Reference [8 0 R] >>\nendobj\n"; @@ -663,9 +618,9 @@ protected function createPdfWithDocMdpNotFirst(): string { } /** - * ISO 32000-1 Table 253: SigRef without /TransformMethod + * SigRef without /TransformMethod (ISO 32000-1 violation) */ - protected function createPdfWithSigRefWithoutTransformMethod(): string { + public static function createPdfWithSigRefWithoutTransformMethod(): string { $pdf = "%PDF-1.7\n"; $pdf .= "1 0 obj\n<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>\nendobj\n"; $pdf .= "2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"; diff --git a/tests/php/fixtures/pdfs/catalog.yaml b/tests/php/fixtures/pdfs/catalog.yaml new file mode 100644 index 0000000000..df26014332 --- /dev/null +++ b/tests/php/fixtures/pdfs/catalog.yaml @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: 2025 LibreCode coop and contributors +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# LibreSign PDF Test Fixtures Catalog +# +# This file describes all PDF test fixtures used in the test suite. +# Each entry provides detailed metadata about the PDF signature characteristics. +# +# Structure: +# filename: The PDF file name (relative to this directory) +# description: Human-readable description +# signature_count: Number of signatures in the PDF +# signatures: Array of signature details +# - signer: Identifier of the signer (email, CN, etc) +# tool: Tool used to create the signature (libresign, jsignpdf, acrobat, etc) +# valid: Boolean indicating if signature is cryptographically valid +# certificate: +# issuer: Certificate issuer +# is_libresign_ca: Whether it's a LibreSign CA certificate +# self_signed: Whether the certificate is self-signed +# features: +# docmdp: DocMDP level (null, 1, 2, 3) +# tsa: Whether it has timestamp authority +# crl: Whether it has CRL (Certificate Revocation List) +# ltv: Whether it has Long Term Validation +# position: Signature position in PDF (1-based index) +# test_expectations: +# should_extract: Whether getCertificateChain should extract it +# should_validate: Whether validation should pass +# expected_modifications: Expected modification status (UNMODIFIED=1, ALLOWED=2, VIOLATION=3) + +pdfs: + - filename: small_valid-signed.pdf + description: Single signature with JSignPDF, basic valid signature + signature_count: 1 + signatures: + - signer: test-user + tool: jsignpdf + valid: true + certificate: + issuer: self-signed + is_libresign_ca: false + self_signed: true + features: + docmdp: null + tsa: false + crl: false + ltv: false + position: 1 + test_expectations: + should_extract: true + should_validate: true + expected_modifications: null + + - filename: real_jsignpdf_level1.pdf + description: JSignPDF signature with DocMDP level 1 (no changes allowed) + signature_count: 1 + signatures: + - signer: jsignpdf-test + tool: jsignpdf + valid: true + certificate: + issuer: self-signed + is_libresign_ca: false + self_signed: true + features: + docmdp: 1 + tsa: false + crl: false + ltv: false + position: 1 + test_expectations: + should_extract: true + should_validate: true + expected_modifications: 1 + + # Template for future test cases: + # - filename: docmdp_level2_dual_signature.pdf + # description: Two signatures with DocMDP level 2 (form filling and signing allowed) + # signature_count: 2 + # signatures: + # - signer: a@example.com + # tool: libresign + # valid: true + # certificate: + # issuer: LibreSign Root CA + # is_libresign_ca: true + # self_signed: false + # features: + # docmdp: 2 + # tsa: true + # crl: true + # ltv: false + # position: 1 + # - signer: b@example.com + # tool: libresign + # valid: true + # certificate: + # issuer: LibreSign Root CA + # is_libresign_ca: true + # self_signed: false + # features: + # docmdp: 2 + # tsa: true + # crl: true + # ltv: false + # position: 2 + # test_expectations: + # should_extract: true + # should_validate: true + # expected_modifications: 2 + +# Additional test cases to create: +# - docmdp_level3_form_filling.pdf: DocMDP level 3 (only form filling allowed) +# - incremental_signature.pdf: Multiple signatures added incrementally +# - invalid_signature_tampered.pdf: Tampered document with broken signature +# - expired_certificate.pdf: Valid signature but expired certificate +# - revoked_certificate.pdf: Signature with revoked certificate +# - tsa_signature.pdf: Signature with timestamp authority +# - ltv_enabled.pdf: Long Term Validation enabled +# - mixed_tools.pdf: Signatures from different tools (LibreSign + Adobe) +# - duplicate_contents.pdf: Edge case with duplicate /Contents (should deduplicate) +# - libresign_ca_signature.pdf: Signature with LibreSign CA +# - external_ca_signature.pdf: Signature with external trusted CA +# - self_signed_untrusted.pdf: Self-signed untrusted certificate diff --git a/tests/php/fixtures/real_jsignpdf_level1.pdf b/tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf similarity index 100% rename from tests/php/fixtures/real_jsignpdf_level1.pdf rename to tests/php/fixtures/pdfs/real_jsignpdf_level1.pdf diff --git a/tests/php/fixtures/small_valid-signed.pdf b/tests/php/fixtures/pdfs/small_valid-signed.pdf similarity index 100% rename from tests/php/fixtures/small_valid-signed.pdf rename to tests/php/fixtures/pdfs/small_valid-signed.pdf diff --git a/tests/php/fixtures/small_valid.pdf b/tests/php/fixtures/pdfs/small_valid.pdf similarity index 100% rename from tests/php/fixtures/small_valid.pdf rename to tests/php/fixtures/pdfs/small_valid.pdf