• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LibreSign / libresign / 18977836254

31 Oct 2025 03:49PM UTC coverage: 39.514%. First build
18977836254

Pull #5704

github

web-flow
Merge 6c1d99483 into 3b0bfae1f
Pull Request #5704: fix: only validate real signatures

33 of 45 new or added lines in 1 file covered. (73.33%)

4505 of 11401 relevant lines covered (39.51%)

2.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.42
/lib/Handler/SignEngine/Pkcs12Handler.php
1
<?php
2

3
declare(strict_types=1);
4
/**
5
 * SPDX-FileCopyrightText: 2020-2024 LibreCode coop and contributors
6
 * SPDX-License-Identifier: AGPL-3.0-or-later
7
 */
8

9
namespace OCA\Libresign\Handler\SignEngine;
10

11
use DateTime;
12
use OCA\Libresign\AppInfo\Application;
13
use OCA\Libresign\Exception\LibresignException;
14
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
15
use OCA\Libresign\Handler\CertificateEngine\OrderCertificatesTrait;
16
use OCA\Libresign\Handler\FooterHandler;
17
use OCA\Libresign\Service\FolderService;
18
use OCA\Libresign\Vendor\phpseclib3\File\ASN1;
19
use OCP\Files\File;
20
use OCP\IAppConfig;
21
use OCP\IL10N;
22
use OCP\ITempManager;
23
use Psr\Log\LoggerInterface;
24

25
class Pkcs12Handler extends SignEngineHandler {
26
        use OrderCertificatesTrait;
27
        protected string $certificate = '';
28
        private array $signaturesFromPoppler = [];
29
        private ?JSignPdfHandler $jSignPdfHandler = null;
30

31
        public function __construct(
32
                private FolderService $folderService,
33
                private IAppConfig $appConfig,
34
                protected CertificateEngineFactory $certificateEngineFactory,
35
                private IL10N $l10n,
36
                private FooterHandler $footerHandler,
37
                private ITempManager $tempManager,
38
                private LoggerInterface $logger,
39
        ) {
40
                parent::__construct($l10n, $folderService, $logger);
51✔
41
        }
42

43
        /**
44
         * @throws LibresignException When is not a signed file
45
         */
46
        private function getSignatures($resource): iterable {
47
                $content = stream_get_contents($resource);
6✔
48
                preg_match_all(
6✔
49
                        '/ByteRange\s*\[\s*(?<offset1>\d+)\s+(?<length1>\d+)\s+(?<offset2>\d+)\s+(?<length2>\d+)\s*\]/',
6✔
50
                        $content,
6✔
51
                        $bytes
6✔
52
                );
6✔
53
                if (empty($bytes['offset1']) || empty($bytes['length1']) || empty($bytes['offset2']) || empty($bytes['length2'])) {
6✔
54
                        throw new LibresignException($this->l10n->t('Unsigned file.'));
4✔
55
                }
56

57
                for ($i = 0; $i < count($bytes['offset1']); $i++) {
2✔
58
                        $offset1 = (int)$bytes['offset1'][$i];
2✔
59
                        $length1 = (int)$bytes['length1'][$i];
2✔
60
                        $offset2 = (int)$bytes['offset2'][$i];
2✔
61

62
                        $signatureStart = $offset1 + $length1 + 1;
2✔
63
                        $signatureLength = $offset2 - $signatureStart - 1;
2✔
64

65
                        rewind($resource);
2✔
66

67
                        $signature = stream_get_contents($resource, $signatureLength, $signatureStart);
2✔
68

69
                        yield hex2bin($signature);
2✔
70
                }
71

72
                $this->tempManager->clean();
2✔
73
        }
74

75
        /**
76
         * @param resource $resource
77
         * @throws LibresignException When is not a signed file
78
         * @return array
79
         */
80
        #[\Override]
81
        public function getCertificateChain($resource): array {
82
                $certificates = [];
6✔
83

84
                foreach ($this->getSignatures($resource) as $signature) {
6✔
85
                        $certificates[] = $this->processSignature($resource, $signature);
2✔
86
                }
87
                return $certificates;
2✔
88
        }
89

90
        private function processSignature($resource, ?string $signature): array {
91
                $result = [];
2✔
92

93
                if (!$signature) {
2✔
94
                        $result['chain'][0]['signature_validation'] = $this->getReadableSigState('Digest Mismatch.');
×
95
                        return $result;
×
96
                }
97

98
                $decoded = ASN1::decodeBER($signature);
2✔
99
                $result = $this->extractTimestampData($decoded, $result);
2✔
100

101
                $chain = $this->extractCertificateChain($signature);
2✔
102
                if (!empty($chain)) {
2✔
103
                        $result['chain'] = $this->orderCertificates($chain);
2✔
104
                        $result = $this->enrichLeafWithPopplerData($resource, $result);
2✔
105
                }
106
                return $result;
2✔
107
        }
108

109
        private function extractTimestampData(array $decoded, array $result): array {
110
                $tsa = new TSA();
2✔
111

112
                try {
113
                        $timestampData = $tsa->extract($decoded);
2✔
114
                        if (!empty($timestampData['genTime']) || !empty($timestampData['policy']) || !empty($timestampData['serialNumber'])) {
2✔
115
                                $result['timestamp'] = $timestampData;
2✔
116
                        }
117
                } catch (\Throwable $e) {
×
118
                }
119

120
                if (!isset($result['signingTime']) || !$result['signingTime'] instanceof \DateTime) {
2✔
121
                        $result['signingTime'] = $tsa->getSigninTime($decoded);
2✔
122
                }
123
                return $result;
2✔
124
        }
125

126
        private function extractCertificateChain(string $signature): array {
127
                $pkcs7PemSignature = $this->der2pem($signature);
2✔
128
                $pemCertificates = [];
2✔
129

130
                if (!openssl_pkcs7_read($pkcs7PemSignature, $pemCertificates)) {
2✔
131
                        return [];
×
132
                }
133

134
                $chain = [];
2✔
135
                foreach ($pemCertificates as $index => $pemCertificate) {
2✔
136
                        $parsed = openssl_x509_parse($pemCertificate);
2✔
137
                        if ($parsed) {
2✔
138
                                $parsed['signature_validation'] = [
2✔
139
                                        'id' => 1,
2✔
140
                                        'label' => $this->l10n->t('Signature is valid.'),
2✔
141
                                ];
2✔
142
                                $chain[$index] = $parsed;
2✔
143
                        }
144
                }
145

146
                return $chain;
2✔
147
        }
148

149
        private function enrichLeafWithPopplerData($resource, array $result): array {
150
                if (empty($result['chain'])) {
2✔
NEW
151
                        return $result;
×
152
                }
153

154
                $popplerOnlyFields = ['field', 'range', 'certificate_validation'];
2✔
155
                if (!isset($result['chain'][0]['subject'])) {
2✔
NEW
156
                        return $result;
×
157
                }
158
                $needPoppler = false;
2✔
159
                foreach ($popplerOnlyFields as $field) {
2✔
160
                        if (empty($result['chain'][0][$field])) {
2✔
161
                                $needPoppler = true;
2✔
162
                                break;
2✔
163
                        }
164
                }
165
                if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
2✔
NEW
166
                        $needPoppler = true;
×
167
                }
168
                if (!$needPoppler) {
2✔
NEW
169
                        return $result;
×
170
                }
171
                $popplerChain = $this->chainFromPoppler($result['chain'][0]['subject'], $resource);
2✔
172
                if (empty($popplerChain)) {
2✔
NEW
173
                        return $result;
×
174
                }
175
                foreach ($popplerOnlyFields as $field) {
2✔
176
                        if (isset($popplerChain[$field])) {
2✔
177
                                $result['chain'][0][$field] = $popplerChain[$field];
2✔
178
                        }
179
                }
180
                if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
2✔
NEW
181
                        if (isset($popplerChain['signature_validation'])) {
×
NEW
182
                                $result['chain'][0]['signature_validation'] = $popplerChain['signature_validation'];
×
183
                        }
184
                }
185
                return $result;
2✔
186
        }
187

188
        private function chainFromPoppler(array $subject, $resource): array {
189
                $fromFallback = $this->popplerUtilsPdfSignFallback($resource);
2✔
190
                foreach ($fromFallback as $popplerSig) {
2✔
191
                        if (!isset($popplerSig['chain'][0]['subject'])) {
2✔
NEW
192
                                continue;
×
193
                        }
194
                        if ($popplerSig['chain'][0]['subject'] == $subject) {
2✔
195
                                return $popplerSig['chain'][0];
2✔
196
                        }
197
                }
NEW
198
                return [];
×
199
        }
200

201
        private function popplerUtilsPdfSignFallback($resource): array {
202
                if (!empty($this->signaturesFromPoppler)) {
2✔
203
                        return $this->signaturesFromPoppler;
1✔
204
                }
205
                if (shell_exec('which pdfsig') === null) {
1✔
NEW
206
                        return $this->signaturesFromPoppler;
×
207
                }
208
                rewind($resource);
1✔
209
                $content = stream_get_contents($resource);
1✔
210
                $tempFile = $this->tempManager->getTemporaryFile('file.pdf');
1✔
211
                file_put_contents($tempFile, $content);
1✔
212

213
                $content = shell_exec('env TZ=UTC pdfsig ' . $tempFile);
1✔
214
                if (empty($content)) {
1✔
NEW
215
                        return $this->signaturesFromPoppler;
×
216
                }
217
                $lines = explode("\n", $content);
1✔
218

219
                $lastSignature = 0;
1✔
220
                foreach ($lines as $item) {
1✔
221
                        $isFirstLevel = preg_match('/^Signature\s#(\d)/', $item, $match);
1✔
222
                        if ($isFirstLevel) {
1✔
223
                                $lastSignature = (int)$match[1] - 1;
1✔
224
                                $this->signaturesFromPoppler[$lastSignature] = [];
1✔
225
                                continue;
1✔
226
                        }
227

228
                        $match = [];
1✔
229
                        $isSecondLevel = preg_match('/^\s+-\s(?<key>.+):\s(?<value>.*)/', $item, $match);
1✔
230
                        if ($isSecondLevel) {
1✔
231
                                switch ((string)$match['key']) {
1✔
232
                                        case 'Signing Time':
1✔
233
                                                $this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC'));
1✔
234
                                                break;
1✔
235
                                        case 'Signer full Distinguished Name':
1✔
236
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']);
1✔
237
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['name'] = $match['value'];
1✔
238
                                                break;
1✔
239
                                        case 'Signing Hash Algorithm':
1✔
240
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signatureTypeSN'] = $match['value'];
1✔
241
                                                break;
1✔
242
                                        case 'Signature Validation':
1✔
243
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signature_validation'] = $this->getReadableSigState($match['value']);
1✔
244
                                                break;
1✔
245
                                        case 'Certificate Validation':
1✔
246
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['certificate_validation'] = $this->getReadableCertState($match['value']);
1✔
247
                                                break;
1✔
248
                                        case 'Signed Ranges':
1✔
249
                                                if (preg_match('/\[(\d+) - (\d+)\], \[(\d+) - (\d+)\]/', $match['value'], $ranges)) {
1✔
250
                                                        $this->signaturesFromPoppler[$lastSignature]['chain'][0]['range'] = [
1✔
251
                                                                'offset1' => (int)$ranges[1],
1✔
252
                                                                'length1' => (int)$ranges[2],
1✔
253
                                                                'offset2' => (int)$ranges[3],
1✔
254
                                                                'length2' => (int)$ranges[4],
1✔
255
                                                        ];
1✔
256
                                                }
257
                                                break;
1✔
258
                                        case 'Signature Field Name':
1✔
259
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['field'] = $match['value'];
1✔
260
                                                break;
1✔
261
                                        case 'Signature Validation':
1✔
262
                                        case 'Signature Type':
1✔
263
                                        case 'Total document signed':
1✔
264
                                        case 'Not total document signed':
1✔
265
                                        default:
266
                                                break;
1✔
267
                                }
268
                        }
269
                }
270
                return $this->signaturesFromPoppler;
1✔
271
        }
272

273
        private function getReadableSigState(string $status) {
274
                return match ($status) {
1✔
275
                        'Signature is Valid.' => [
1✔
276
                                'id' => 1,
1✔
277
                                'label' => $this->l10n->t('Signature is valid.'),
1✔
278
                        ],
1✔
279
                        'Signature is Invalid.' => [
×
280
                                'id' => 2,
×
281
                                'label' => $this->l10n->t('Signature is invalid.'),
×
282
                        ],
×
283
                        'Digest Mismatch.' => [
×
284
                                'id' => 3,
×
285
                                'label' => $this->l10n->t('Digest mismatch.'),
×
286
                        ],
×
287
                        "Document isn't signed or corrupted data." => [
×
288
                                'id' => 4,
×
289
                                'label' => $this->l10n->t("Document isn't signed or corrupted data."),
×
290
                        ],
×
291
                        'Signature has not yet been verified.' => [
×
292
                                'id' => 5,
×
293
                                'label' => $this->l10n->t('Signature has not yet been verified.'),
×
294
                        ],
×
295
                        default => [
1✔
296
                                'id' => 6,
1✔
297
                                'label' => $this->l10n->t('Unknown validation failure.'),
1✔
298
                        ],
1✔
299
                };
1✔
300
        }
301

302

303
        private function getReadableCertState(string $status) {
304
                return match ($status) {
1✔
305
                        'Certificate is Trusted.' => [
×
306
                                'id' => 1,
×
307
                                'label' => $this->l10n->t('Certificate is trusted.'),
×
308
                        ],
×
309
                        "Certificate issuer isn't Trusted." => [
×
310
                                'id' => 2,
×
311
                                'label' => $this->l10n->t("Certificate issuer isn't trusted."),
×
312
                        ],
×
313
                        'Certificate issuer is unknown.' => [
1✔
314
                                'id' => 3,
1✔
315
                                'label' => $this->l10n->t('Certificate issuer is unknown.'),
1✔
316
                        ],
1✔
317
                        'Certificate has been Revoked.' => [
×
318
                                'id' => 4,
×
319
                                'label' => $this->l10n->t('Certificate has been revoked.'),
×
320
                        ],
×
321
                        'Certificate has Expired' => [
×
322
                                'id' => 5,
×
323
                                'label' => $this->l10n->t('Certificate has expired'),
×
324
                        ],
×
325
                        'Certificate has not yet been verified.' => [
×
326
                                'id' => 6,
×
327
                                'label' => $this->l10n->t('Certificate has not yet been verified.'),
×
328
                        ],
×
329
                        default => [
1✔
330
                                'id' => 7,
1✔
331
                                'label' => $this->l10n->t('Unknown issue with Certificate or corrupted data.')
1✔
332
                        ],
1✔
333
                };
1✔
334
        }
335

336

337
        private function parseDistinguishedNameWithMultipleValues(string $dn): array {
338
                $result = [];
1✔
339
                $pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn);
1✔
340

341
                foreach ($pairs as $pair) {
1✔
342
                        [$key, $value] = explode('=', $pair, 2);
1✔
343
                        if (empty($key) || empty($value)) {
1✔
NEW
344
                                return $result;
×
345
                        }
346
                        $key = trim($key);
1✔
347
                        $value = trim($value);
1✔
348
                        $value = trim($value, '"');
1✔
349

350
                        if (!isset($result[$key])) {
1✔
351
                                $result[$key] = $value;
1✔
352
                        } else {
353
                                if (!is_array($result[$key])) {
×
354
                                        $result[$key] = [$result[$key]];
×
355
                                }
356
                                $result[$key][] = $value;
×
357
                        }
358
                }
359

360
                return $result;
1✔
361
        }
362

363
        private function der2pem($derData) {
364
                $pem = chunk_split(base64_encode((string)$derData), 64, "\n");
2✔
365
                $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
2✔
366
                return $pem;
2✔
367
        }
368

369
        private function getHandler(): SignEngineHandler {
370
                $sign_engine = $this->appConfig->getValueString(Application::APP_ID, 'sign_engine', 'JSignPdf');
1✔
371
                $property = lcfirst($sign_engine) . 'Handler';
1✔
372
                if (!property_exists($this, $property)) {
1✔
373
                        throw new LibresignException($this->l10n->t('Invalid Sign engine.'), 400);
×
374
                }
375
                $classHandler = 'OCA\\Libresign\\Handler\\SignEngine\\' . ucfirst($property);
1✔
376
                if (!$this->$property instanceof $classHandler) {
1✔
377
                        $this->$property = \OCP\Server::get($classHandler);
1✔
378
                }
379
                return $this->$property;
1✔
380
        }
381

382
        #[\Override]
383
        public function sign(): File {
384
                $signedContent = $this->getHandler()
1✔
385
                        ->setCertificate($this->getCertificate())
1✔
386
                        ->setInputFile($this->getInputFile())
1✔
387
                        ->setPassword($this->getPassword())
1✔
388
                        ->setSignatureParams($this->getSignatureParams())
1✔
389
                        ->setVisibleElements($this->getVisibleElements())
1✔
390
                        ->getSignedContent();
1✔
391
                $this->getInputFile()->putContent($signedContent);
×
392
                return $this->getInputFile();
×
393
        }
394

395
        public function isHandlerOk(): bool {
396
                return $this->certificateEngineFactory->getEngine()->isSetupOk();
1✔
397
        }
398
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc