Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
23 / 23 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
| HashAlgorithmResolver | |
100.00% |
23 / 23 |
|
100.00% |
7 / 7 |
13 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| forSignature | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| forTsa | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
| requiresPdfVersionUpgradeForSha256 | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| forPdfVersion | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| validate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| getConfiguredAlgorithm | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | /** |
| 5 | * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors |
| 6 | * SPDX-License-Identifier: AGPL-3.0-or-later |
| 7 | */ |
| 8 | |
| 9 | namespace OCA\Libresign\Handler\SignEngine\JSignPdf; |
| 10 | |
| 11 | use OCA\Libresign\Service\Policy\PolicyService; |
| 12 | use OCA\Libresign\Service\Policy\Provider\SignatureHashAlgorithm\SignatureHashAlgorithmPolicy; |
| 13 | use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicy; |
| 14 | use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicyValue; |
| 15 | |
| 16 | /** |
| 17 | * Resolves which hash algorithm JSignPdf has to use. |
| 18 | * |
| 19 | * The algorithm that signs the document depends on the PDF version, so it is |
| 20 | * not a valid answer for the other hashes JSignPdf takes: each one needs its |
| 21 | * own method here. |
| 22 | */ |
| 23 | class HashAlgorithmResolver { |
| 24 | private const float MIN_PDF_VERSION_SHA256 = 1.6; |
| 25 | private const float MIN_PDF_VERSION_SHA1_REJECT = 1.7; |
| 26 | private const string DEFAULT_ALGORITHM = 'SHA256'; |
| 27 | /** @var string[] */ |
| 28 | private const array SUPPORTED_ALGORITHMS = ['SHA1', 'SHA256', 'SHA384', 'SHA512', 'RIPEMD160']; |
| 29 | |
| 30 | public function __construct( |
| 31 | private PolicyService $policyService, |
| 32 | ) { |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Algorithm used to sign a document with the given PDF version. |
| 37 | * |
| 38 | * @param float|null $pdfVersion null when the content carries no readable PDF header |
| 39 | */ |
| 40 | public function forSignature(?float $pdfVersion): string { |
| 41 | $configuredAlgorithm = $this->getConfiguredAlgorithm(); |
| 42 | /** |
| 43 | * Need to respect the follow code: |
| 44 | * https://github.com/intoolswetrust/jsignpdf/blob/JSignPdf_2_2_2/jsignpdf/src/main/java/net/sf/jsignpdf/types/HashAlgorithm.java#L46-L47 |
| 45 | */ |
| 46 | if ($pdfVersion === null) { |
| 47 | return $this->validate($configuredAlgorithm); |
| 48 | } |
| 49 | |
| 50 | return $this->forPdfVersion($pdfVersion, $configuredAlgorithm); |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * Algorithm of the timestamp query, taken from the TSA policy. |
| 55 | * |
| 56 | * It is never derived from the signature: that one depends on the PDF |
| 57 | * version and would ask the authority for SHA1 whenever the document is |
| 58 | * older than PDF 1.6, which is the rejection reported in #8145. |
| 59 | */ |
| 60 | public function forTsa(): string { |
| 61 | $tsaSettings = TsaPolicyValue::decode($this->policyService->resolve(TsaPolicy::KEY)->getEffectiveValue()); |
| 62 | |
| 63 | // JSignPdf spells the TSA algorithm with a hyphen, unlike --hash-algorithm. |
| 64 | return match ($tsaSettings['hash_algorithm']) { |
| 65 | 'SHA384' => 'SHA-384', |
| 66 | 'SHA512' => 'SHA-512', |
| 67 | default => 'SHA-256', |
| 68 | }; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * PDFs older than 1.6 have to be upgraded before JSignPdf accepts SHA-256. |
| 73 | */ |
| 74 | public function requiresPdfVersionUpgradeForSha256(float $pdfVersion): bool { |
| 75 | if ($pdfVersion >= self::MIN_PDF_VERSION_SHA256) { |
| 76 | return false; |
| 77 | } |
| 78 | |
| 79 | return $this->getConfiguredAlgorithm() === self::DEFAULT_ALGORITHM; |
| 80 | } |
| 81 | |
| 82 | private function forPdfVersion(float $pdfVersion, string $configuredAlgorithm): string { |
| 83 | // Legacy compatibility: JSignPdf still requires SHA1 for very old PDFs (< 1.6). |
| 84 | // The policy still exposes SHA1 for supported legacy workflows, and the runtime |
| 85 | // must continue enforcing this fallback for ancient PDFs that JSignPdf cannot sign otherwise. |
| 86 | if ($pdfVersion < self::MIN_PDF_VERSION_SHA256) { |
| 87 | return 'SHA1'; |
| 88 | } |
| 89 | if ($pdfVersion < self::MIN_PDF_VERSION_SHA1_REJECT) { |
| 90 | return self::DEFAULT_ALGORITHM; |
| 91 | } |
| 92 | if ($configuredAlgorithm === 'SHA1') { |
| 93 | return self::DEFAULT_ALGORITHM; |
| 94 | } |
| 95 | |
| 96 | return $this->validate($configuredAlgorithm); |
| 97 | } |
| 98 | |
| 99 | private function validate(string $algorithm): string { |
| 100 | return in_array($algorithm, self::SUPPORTED_ALGORITHMS, true) ? $algorithm : self::DEFAULT_ALGORITHM; |
| 101 | } |
| 102 | |
| 103 | private function getConfiguredAlgorithm(): string { |
| 104 | return (string)$this->policyService->resolve(SignatureHashAlgorithmPolicy::KEY)->getEffectiveValue(); |
| 105 | } |
| 106 | } |