Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.31% covered (success)
98.31%
58 / 59
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
Password
98.31% covered (success)
98.31%
58 / 59
90.00% covered (success)
90.00%
9 / 10
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 validateToSign
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validateCertificateRevocation
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 logRevocationBlockedSigning
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getRevocationErrorMessage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 validateCertificateExpiration
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 validateToIdentify
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readCurrentSignerCertificate
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
 toArray
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 hasSignatureFile
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Service\IdentifyMethod\SignatureMethod;
10
11use OCA\Libresign\Enum\CrlValidationStatus;
12use OCA\Libresign\Exception\InvalidPasswordException;
13use OCA\Libresign\Exception\LibresignException;
14use OCA\Libresign\Handler\SignEngine\Pkcs12Handler;
15use OCA\Libresign\Service\IdentifyMethod\IdentifyService;
16use OCP\IUserSession;
17
18class Password extends AbstractSignatureMethod {
19    public function __construct(
20        protected IdentifyService $identifyService,
21        protected Pkcs12Handler $pkcs12Handler,
22        private IUserSession $userSession,
23    ) {
24        // TRANSLATORS Name of possible authenticator method. This signalize that the signer could be identified by certificate password
25        $this->friendlyName = $this->identifyService->getL10n()->t('Certificate with password');
26        parent::__construct(
27            $identifyService,
28        );
29    }
30
31    #[\Override]
32    public function validateToSign(): void {
33        $certificateData = $this->readCurrentSignerCertificate();
34        $this->validateCertificateRevocation($certificateData);
35        $this->validateCertificateExpiration($certificateData);
36    }
37
38    private function validateCertificateRevocation(array $certificateData): void {
39        if (!array_key_exists('crl_validation', $certificateData)) {
40            return;
41        }
42        $status = $certificateData['crl_validation'];
43        if ($status === CrlValidationStatus::VALID) {
44            return;
45        }
46        if ($status === CrlValidationStatus::REVOKED) {
47            // TRANSLATORS Error shown when signing is blocked because the signer certificate was revoked.
48            throw new LibresignException($this->identifyService->getL10n()->t('Certificate has been revoked'), 422);
49        }
50        // Admin explicitly disabled external CRL validation â€“ allow signing.
51        if ($status === CrlValidationStatus::DISABLED) {
52            return;
53        }
54        $this->logRevocationBlockedSigning($status);
55        throw new LibresignException($this->getRevocationErrorMessage($status), 422);
56    }
57
58    private function logRevocationBlockedSigning(CrlValidationStatus $status): void {
59        $this->identifyService->getLogger()->warning('Signing blocked due to CRL validation status', [
60            'status' => $status->value,
61            'signer_uid' => $this->userSession->getUser()?->getUID(),
62        ]);
63    }
64
65    private function getRevocationErrorMessage(CrlValidationStatus $status): string {
66        return match ($status) {
67            // TRANSLATORS Error when a certificate contains LibreSign's obsolete CRL URL, which cannot identify the CA generation safely.
68            CrlValidationStatus::LEGACY_DISTRIBUTION_POINT => $this->identifyService->getL10n()->t('This signing certificate uses an outdated revocation URL. Regenerate your signing certificate before signing.'),
69            // TRANSLATORS Error when the CRL distribution points (URLs) cannot be reached to check if certificate is revoked
70            CrlValidationStatus::URLS_INACCESSIBLE => $this->identifyService->getL10n()->t('Cannot reach the certificate revocation service. Signing is not allowed.'),
71            // TRANSLATORS Error when an error occurs during certificate revocation status validation
72            CrlValidationStatus::VALIDATION_ERROR => $this->identifyService->getL10n()->t('An error occurred during certificate validation. Signing is not allowed.'),
73            // TRANSLATORS Error when certificate validation completed but could not determine if certificate is revoked
74            CrlValidationStatus::VALIDATION_FAILED => $this->identifyService->getL10n()->t('Certificate validation failed. Signing is not allowed. Contact your administrator.'),
75            // TRANSLATORS Error when certificate has no CRL distribution points (URLs to check revocation)
76            CrlValidationStatus::NO_URLS => $this->identifyService->getL10n()->t('This certificate has no revocation URLs. Signing is not allowed. Contact your administrator.'),
77            // TRANSLATORS Error when certificate has no revocation information configured
78            CrlValidationStatus::MISSING => $this->identifyService->getL10n()->t('This certificate has no revocation information. Signing is not allowed. Contact your administrator.'),
79            default => $this->identifyService->getL10n()->t('Certificate validation could not be completed. Signing is not allowed.'),
80        };
81    }
82
83    private function validateCertificateExpiration(array $certificateData): void {
84        if (array_key_exists('validTo_time_t', $certificateData)) {
85            $validTo = $certificateData['validTo_time_t'];
86            if (!is_int($validTo)) {
87                // TRANSLATORS Error shown when the signing certificate data is invalid.
88                throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'), 422);
89            }
90            $now = (new \DateTime())->getTimestamp();
91            if ($validTo <= $now) {
92                // TRANSLATORS Error shown when signing is blocked because the signer certificate has expired.
93                throw new LibresignException($this->identifyService->getL10n()->t('Certificate has expired'), 422);
94            }
95        }
96    }
97
98    #[\Override]
99    public function validateToIdentify(): void {
100        $this->readCurrentSignerCertificate();
101    }
102
103    private function readCurrentSignerCertificate(): array {
104        $uid = $this->userSession->getUser()?->getUID();
105        if ($uid === null || $uid === '') {
106            throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'));
107        }
108
109        $pfx = $this->pkcs12Handler->getPfxOfCurrentSigner($uid);
110        if (empty($pfx)) {
111            // TRANSLATORS Error shown when the signing certificate file for the current user is missing or invalid.
112            throw new LibresignException($this->identifyService->getL10n()->t('Invalid certificate'));
113        }
114
115        try {
116            return $this->pkcs12Handler
117                ->setCertificate($pfx)
118                ->setPassword($this->codeSentByUser)
119                ->readCertificate();
120        } catch (InvalidPasswordException) {
121            // TRANSLATORS Error shown when the password used to unlock the signing certificate is incorrect.
122            throw new LibresignException($this->identifyService->getL10n()->t('Invalid password'));
123        }
124    }
125
126    #[\Override]
127    public function toArray(): array {
128        $return = parent::toArray();
129        $return['hasSignatureFile'] = $this->hasSignatureFile();
130        return $return;
131    }
132
133    private function hasSignatureFile(): bool {
134        $uid = $this->userSession->getUser()?->getUID();
135        if ($uid === null || $uid === '') {
136            return false;
137        }
138        try {
139            $this->pkcs12Handler->getPfxOfCurrentSigner($uid);
140            return true;
141        } catch (\Throwable) {
142        }
143        return false;
144    }
145}