From be351a9e98575e1ebc19b4dcd3cb8d872c8e832e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 1/8] fix(account): revoke user certificates when deleting pfx Call CRL revocation by user UID before deleting the PFX file. Use CESSATION_OF_OPERATION with a reason description to keep auditability when certificates are removed by the account owner. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/AccountService.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 4742dd25db..9c822093f9 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -17,6 +17,7 @@ use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Db\UserElement; use OCA\Libresign\Db\UserElementMapper; +use OCA\Libresign\Enum\CRLReason; use OCA\Libresign\Enum\FileStatus; use OCA\Libresign\Exception\InvalidPasswordException; use OCA\Libresign\Exception\LibresignException; @@ -24,6 +25,7 @@ use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; +use OCA\Libresign\Service\Crl\CrlService; use OCA\Settings\Mailer\NewUserMailHelper; use OCP\Accounts\IAccountManager; use OCP\AppFramework\Db\DoesNotExistException; @@ -78,6 +80,7 @@ public function __construct( private IClientService $clientService, private ITimeFactory $timeFactory, private FileUploadHelper $uploadHelper, + private CrlService $crlService, ) { } @@ -565,7 +568,16 @@ public function uploadPfx(array $file, IUser $user): void { } public function deletePfx(IUser $user): void { - $this->pkcs12Handler->deletePfx($user->getUID()); + $uid = $user->getUID(); + + $this->crlService->revokeUserCertificates( + $uid, + CRLReason::CESSATION_OF_OPERATION, + 'Certificate deleted by account owner.', + $uid, + ); + + $this->pkcs12Handler->deletePfx($uid); } /** From 952cd8c41c06e6dc10e67facd5d98919152a6e4d Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 2/8] fix(signature): return 422 for certificate validation failures Differentiate certificate validation errors from missing-signature-password flow.\n\nUse HTTP 422 for revoked, invalid, expired, and unverifiable revocation status cases to avoid incorrect UI actions. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/IdentifyMethod/SignatureMethod/Password.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/IdentifyMethod/SignatureMethod/Password.php b/lib/Service/IdentifyMethod/SignatureMethod/Password.php index 264d515783..ecb3e2ed34 100644 --- a/lib/Service/IdentifyMethod/SignatureMethod/Password.php +++ b/lib/Service/IdentifyMethod/SignatureMethod/Password.php @@ -63,7 +63,7 @@ private function validateCertificateRevocation(array $certificateData): void { // fail-closed – we cannot confirm the certificate is not revoked. throw new LibresignException( $this->identifyService->getL10n()->t('Certificate revocation status could not be verified'), - 400 + 422 ); } @@ -71,11 +71,11 @@ private function validateCertificateExpiration(array $certificateData): void { if (array_key_exists('validTo_time_t', $certificateData)) { $validTo = $certificateData['validTo_time_t']; if (!is_int($validTo)) { - throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'), 422); } $now = (new \DateTime())->getTimestamp(); if ($validTo <= $now) { - throw new LibresignException($this->identifyService->getL10n()->t('Certificate has expired'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Certificate has expired'), 422); } } } From 5c2c5fa7de5b484c16718cdb43ac7cb943d0aa6b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 3/8] test(store): cover 422 certificate sign error parsing Add test to ensure certificate validation API errors are kept as signError. Assert the store preserves the original API message list and does not infer missing-certification actions. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/store/sign.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tests/store/sign.spec.ts b/src/tests/store/sign.spec.ts index b0317a2806..904b400f87 100644 --- a/src/tests/store/sign.spec.ts +++ b/src/tests/store/sign.spec.ts @@ -357,6 +357,20 @@ describe('useSignStore', () => { expect(result.type).toBe('signError') expect(result.errors).toEqual(['err']) }) + + it('returns signError for certificate validation errors and preserves API message', () => { + const store = useSignStore() + const apiErrors = [{ message: 'Certificate has been revoked', code: 422 }] + const error = { + response: { data: { ocs: { data: { errors: apiErrors } } } }, + } + + const result = store.parseSignError(error) + + expect(result.type).toBe('signError') + expect(result.action).toBeUndefined() + expect(result.errors).toEqual(apiErrors) + }) }) describe('setFileToSign', () => { From 53372d38a453a6c1a47fc2047d57d524e3dd7313 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 4/8] test(sign-view): keep certificate revocation errors in UI state Add submitSignature test for API signError with revoked certificate response. Verify the certificate modal is not opened and signing errors are stored for user feedback. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/tests/views/SignPDF/Sign.spec.ts | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/tests/views/SignPDF/Sign.spec.ts b/src/tests/views/SignPDF/Sign.spec.ts index fbfc05ee12..0a1a64168f 100644 --- a/src/tests/views/SignPDF/Sign.spec.ts +++ b/src/tests/views/SignPDF/Sign.spec.ts @@ -400,6 +400,53 @@ describe('Sign.vue - signWithTokenCode', () => { }) }) + describe('Sign.vue - API error handling', () => { + it('keeps certificate validation errors in signStore and does not open certificate modal', async () => { + const SignComponent = await import('../../../views/SignPDF/_partials/Sign.vue') + const submitSignature = (SignComponent.default as any).methods.submitSignature + + const apiErrors = [{ message: 'Certificate has been revoked', code: 422 }] + const context = { + loading: false, + elements: [], + canCreateSignature: false, + signRequestUuid: 'test-sign-request-uuid', + signMethodsStore: { + certificateEngine: 'openssl', + }, + signatureElementsStore: { + signs: {}, + }, + actionHandler: { + showModal: vi.fn(), + closeModal: vi.fn(), + }, + signStore: { + document: { id: 10 }, + clearSigningErrors: vi.fn(), + setSigningErrors: vi.fn(), + submitSignature: vi.fn().mockRejectedValue({ + type: 'signError', + errors: apiErrors, + }), + }, + $emit: vi.fn(), + sidebarStore: { + hideSidebar: vi.fn(), + }, + } + + await submitSignature.call(context, { + method: 'password', + token: '123456', + }) + + expect(context.actionHandler.showModal).not.toHaveBeenCalled() + expect(context.signStore.setSigningErrors).toHaveBeenCalledWith(apiErrors) + expect(context.loading).toBe(false) + }) + }) + describe('proceedWithSigning - Full flow with WhatsApp token', () => { let proceedWithSigningLogic: ProceedWithSigningLogic From ab57b2d54a1e73f1e7f21fe62d6909f6b287e657 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 5/8] test(signing-handler): assert 422 revoked maps to do-nothing Cover handler behavior for certificate revocation errors returned as code 422. Ensure these errors do not trigger signature password creation actions. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Handler/SigningErrorHandlerTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/php/Unit/Handler/SigningErrorHandlerTest.php b/tests/php/Unit/Handler/SigningErrorHandlerTest.php index 82fb4d98c7..7d2fd1178c 100644 --- a/tests/php/Unit/Handler/SigningErrorHandlerTest.php +++ b/tests/php/Unit/Handler/SigningErrorHandlerTest.php @@ -46,6 +46,11 @@ public static function libresignExceptionProvider(): array { 'message' => 'Password required', 'expectedAction' => JSActions::ACTION_CREATE_SIGNATURE_PASSWORD, ], + 'code 422 revoked certificate triggers do nothing action' => [ + 'code' => 422, + 'message' => 'Certificate has been revoked', + 'expectedAction' => JSActions::ACTION_DO_NOTHING, + ], 'code 401 triggers do nothing action' => [ 'code' => 401, 'message' => 'Unauthorized', From 6a2f470984cc96880015b3989d14ed547ac4db1b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 6/8] test(account): validate revocation call on pfx deletion Inject CrlService in AccountService test setup and assert deletePfx behavior. Verify revocation is called with owner UID, reason code, and reason text before deleting the PFX. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- tests/php/Unit/Service/AccountServiceTest.php | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/php/Unit/Service/AccountServiceTest.php b/tests/php/Unit/Service/AccountServiceTest.php index 0b19ad17da..6f20d3cf20 100644 --- a/tests/php/Unit/Service/AccountServiceTest.php +++ b/tests/php/Unit/Service/AccountServiceTest.php @@ -17,11 +17,13 @@ use OCA\Libresign\Db\SignRequestMapper; use OCA\Libresign\Db\UserElement; use OCA\Libresign\Db\UserElementMapper; +use OCA\Libresign\Enum\CRLReason; use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory; use OCA\Libresign\Handler\SignEngine\Pkcs12Handler; use OCA\Libresign\Helper\FileUploadHelper; use OCA\Libresign\Helper\ValidateHelper; use OCA\Libresign\Service\AccountService; +use OCA\Libresign\Service\Crl\CrlService; use OCA\Libresign\Service\FolderService; use OCA\Libresign\Service\IdDocsService; use OCA\Libresign\Service\IdentifyMethod\IIdentifyMethod; @@ -81,6 +83,7 @@ final class AccountServiceTest extends \OCA\Libresign\Tests\Unit\TestCase { private RequestSignatureService&MockObject $requestSignatureService; private Pkcs12Handler&MockObject $pkcs12Handler; private FileUploadHelper&MockObject $uploadHelper; + private CrlService&MockObject $crlService; public function setUp(): void { parent::setUp(); @@ -115,6 +118,7 @@ public function setUp(): void { $this->clientService = $this->createMock(ClientService::class); $this->timeFactory = $this->createMock(TimeFactory::class); $this->uploadHelper = $this->createMock(FileUploadHelper::class); + $this->crlService = $this->createMock(CrlService::class); } private function getService(): AccountService { @@ -146,10 +150,32 @@ private function getService(): AccountService { $this->folderService, $this->clientService, $this->timeFactory, - $this->uploadHelper + $this->uploadHelper, + $this->crlService ); } + public function testDeletePfxRevokesCertificatesWithReasonAndDeletesPfx(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('admin'); + + $this->crlService->expects($this->once()) + ->method('revokeUserCertificates') + ->with( + 'admin', + CRLReason::CESSATION_OF_OPERATION, + 'Certificate deleted by account owner.', + 'admin' + ) + ->willReturn(1); + + $this->pkcs12Handler->expects($this->once()) + ->method('deletePfx') + ->with('admin'); + + $this->getService()->deletePfx($user); + } + #[DataProvider('provideValidateCertificateDataCases')] public function testValidateCertificateDataUsingDataProvider($arguments, $expectedErrorMessage):void { if (is_callable($arguments)) { From 5d5cd7fde286144fede224431c0ff5d66d0aa922 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:13:24 -0300 Subject: [PATCH 7/8] test(password-identify): assert exception codes for cert errors Expand certificate validation tests to assert LibresignException error codes. Cover 422 for revoked, expired, invalid, and revocation-status-unverifiable certificate scenarios. Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../Unit/Service/IdentifyMethod/PasswordTest.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php index 49d6cb100d..2a3eb1bf5a 100644 --- a/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php +++ b/tests/php/Unit/Service/IdentifyMethod/PasswordTest.php @@ -136,7 +136,12 @@ public static function providerValidateToSignWithError(): array { } #[DataProvider('providerValidateToSignWithCertificateData')] - public function testValidateToSignWithCertificateData(array $certificateData, bool $shouldThrow, string $expectedMessage = ''): void { + public function testValidateToSignWithCertificateData( + array $certificateData, + bool $shouldThrow, + string $expectedMessage = '', + ?int $expectedCode = null, + ): void { $this->pkcs12Handler = $this->getPkcs12Instance(['getPfxOfCurrentSigner', 'setCertificate', 'setPassword', 'readCertificate']); $this->pkcs12Handler->method('getPfxOfCurrentSigner')->willReturn('mock-pfx'); $this->pkcs12Handler->method('setCertificate')->willReturnSelf(); @@ -153,6 +158,9 @@ public function testValidateToSignWithCertificateData(array $certificateData, bo if ($expectedMessage) { $this->expectExceptionMessage($expectedMessage); } + if ($expectedCode !== null) { + $this->expectExceptionCode($expectedCode); + } } $password->validateToSign(); @@ -183,6 +191,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate has expired', + 'expectedCode' => 422, ], 'invalid certificate - validTo_time_t is string' => [ 'certificateData' => [ @@ -190,6 +199,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Invalid certificate', + 'expectedCode' => 422, ], 'invalid certificate - validTo_time_t is null' => [ 'certificateData' => [ @@ -233,6 +243,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate has been revoked', + 'expectedCode' => 422, ], 'valid certificate with crl validation' => [ 'certificateData' => [ @@ -255,6 +266,7 @@ public static function providerValidateToSignWithCertificateData(): array { ], 'shouldThrow' => true, 'expectedMessage' => 'Certificate revocation status could not be verified', + 'expectedCode' => 422, ], 'invalid certificate - crl validation empty string' => [ 'certificateData' => [ From c6d9c6da23ea4615cbdf911b41e5c2addf39d8d2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:32:02 -0300 Subject: [PATCH 8/8] fix: typo at error number Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- lib/Service/IdentifyMethod/SignatureMethod/Password.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/IdentifyMethod/SignatureMethod/Password.php b/lib/Service/IdentifyMethod/SignatureMethod/Password.php index ecb3e2ed34..85bf2170bf 100644 --- a/lib/Service/IdentifyMethod/SignatureMethod/Password.php +++ b/lib/Service/IdentifyMethod/SignatureMethod/Password.php @@ -53,7 +53,7 @@ private function validateCertificateRevocation(array $certificateData): void { return; } if ($status === CrlValidationStatus::REVOKED) { - throw new LibresignException($this->identifyService->getL10n()->t('Certificate has been revoked'), 400); + throw new LibresignException($this->identifyService->getL10n()->t('Certificate has been revoked'), 422); } // Admin explicitly disabled external CRL validation – allow signing. if ($status === CrlValidationStatus::DISABLED) {