Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
HashAlgorithmResolver
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
7 / 7
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forSignature
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 forTsa
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 requiresPdfVersionUpgradeForSha256
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 forPdfVersion
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 validate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getConfiguredAlgorithm
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Handler\SignEngine\JSignPdf;
10
11use OCA\Libresign\Service\Policy\PolicyService;
12use OCA\Libresign\Service\Policy\Provider\SignatureHashAlgorithm\SignatureHashAlgorithmPolicy;
13use OCA\Libresign\Service\Policy\Provider\Tsa\TsaPolicy;
14use 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 */
23class 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}