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

LibreSign / libresign / 19993337372

06 Dec 2025 07:39PM UTC coverage: 42.664%. First build
19993337372

Pull #6012

github

web-flow
Merge 352bc1328 into 9de3c9f7e
Pull Request #6012: Feat/docmdp iso validation

195 of 219 new or added lines in 4 files covered. (89.04%)

5449 of 12772 relevant lines covered (42.66%)

4.75 hits per line

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

70.43
/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\DocMdpHandler;
17
use OCA\Libresign\Handler\FooterHandler;
18
use OCA\Libresign\Service\CaIdentifierService;
19
use OCA\Libresign\Service\FolderService;
20
use OCP\Files\File;
21
use OCP\IAppConfig;
22
use OCP\IL10N;
23
use OCP\ITempManager;
24
use phpseclib3\File\ASN1;
25
use Psr\Log\LoggerInterface;
26

27
class Pkcs12Handler extends SignEngineHandler {
28
        use OrderCertificatesTrait;
29
        protected string $certificate = '';
30
        private array $signaturesFromPoppler = [];
31
        private ?JSignPdfHandler $jSignPdfHandler = null;
32
        private string $rootCertificatePem = '';
33
        private bool $isLibreSignFile = false;
34

35
        public function __construct(
36
                private FolderService $folderService,
37
                private IAppConfig $appConfig,
38
                protected CertificateEngineFactory $certificateEngineFactory,
39
                private IL10N $l10n,
40
                private FooterHandler $footerHandler,
41
                private ITempManager $tempManager,
42
                private LoggerInterface $logger,
43
                private CaIdentifierService $caIdentifierService,
44
                private DocMdpHandler $docMdpHandler,
45
        ) {
46
                parent::__construct($l10n, $folderService, $logger);
52✔
47
        }
48

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

64
                for ($i = 0; $i < count($bytes['offset1']); $i++) {
3✔
65
                        $offset1 = (int)$bytes['offset1'][$i];
3✔
66
                        $length1 = (int)$bytes['length1'][$i];
3✔
67
                        $offset2 = (int)$bytes['offset2'][$i];
3✔
68

69
                        $signatureStart = $offset1 + $length1 + 1;
3✔
70
                        $signatureLength = $offset2 - $signatureStart - 1;
3✔
71

72
                        rewind($resource);
3✔
73

74
                        $signature = stream_get_contents($resource, $signatureLength, $signatureStart);
3✔
75
                        if ($signature === false) {
3✔
76
                                yield null;
×
77
                                continue;
×
78
                        }
79

80
                        $decodedSignature = @hex2bin($signature);
3✔
81
                        if ($decodedSignature === false) {
3✔
82
                                yield null;
1✔
83
                                continue;
1✔
84
                        }
85
                        yield $decodedSignature;
2✔
86
                }
87

88
                $this->tempManager->clean();
3✔
89
        }
90

91
        public function setIsLibreSignFile(): void {
92
                $this->isLibreSignFile = true;
×
93
        }
94

95
        /**
96
         * @param resource $resource
97
         * @throws LibresignException When is not a signed file
98
         * @return array
99
         */
100
        #[\Override]
101
        public function getCertificateChain($resource): array {
102
                $certificates = [];
7✔
103

104
                foreach ($this->getSignatures($resource) as $signature) {
7✔
105
                        $certificates[] = $this->processSignature($resource, $signature);
3✔
106
                }
107
                return $certificates;
3✔
108
        }
109

110
        private function processSignature($resource, ?string $signature): array {
111
                $result = [];
3✔
112

113
                if (!$signature) {
3✔
114
                        $result['chain'][0]['signature_validation'] = $this->getReadableSigState('Digest Mismatch.');
1✔
115
                        return $result;
1✔
116
                }
117

118
                $decoded = ASN1::decodeBER($signature);
2✔
119
                $result = $this->extractTimestampData($decoded, $result);
2✔
120

121
                $chain = $this->extractCertificateChain($signature);
2✔
122
                if (!empty($chain)) {
2✔
123
                        $result['chain'] = $this->orderCertificates($chain);
2✔
124
                        $result = $this->enrichLeafWithPopplerData($resource, $result);
2✔
125
                }
126

127
                $result = $this->extractDocMdpData($resource, $result);
2✔
128

129
                $result = $this->applyLibreSignRootCAFlag($result);
2✔
130
                return $result;
2✔
131
        }
132

133
        private function applyLibreSignRootCAFlag(array $signer): array {
134
                if (empty($signer['chain'])) {
2✔
135
                        return $signer;
×
136
                }
137

138
                foreach ($signer['chain'] as $key => $cert) {
2✔
139
                        if ($cert['isLibreSignRootCA']
2✔
140
                                && $cert['certificate_validation']['id'] !== 1
2✔
141
                        ) {
142
                                $signer['chain'][$key]['certificate_validation'] = [
×
143
                                        'id' => 1,
×
144
                                        'label' => $this->l10n->t('Certificate is trusted.'),
×
145
                                ];
×
146
                        }
147
                }
148

149
                return $signer;
2✔
150
        }
151

152

153
        private function extractDocMdpData($resource, array $result): array {
154
                if (empty($result['chain'])) {
2✔
NEW
155
                        return $result;
×
156
                }
157

158
                $docMdpData = $this->docMdpHandler->extractDocMdpData($resource);
2✔
159
                return array_merge($result, $docMdpData);
2✔
160
        }
161

162
        private function extractTimestampData(array $decoded, array $result): array {
163
                $tsa = new TSA();
2✔
164

165
                try {
166
                        $timestampData = $tsa->extract($decoded);
2✔
167
                        if (!empty($timestampData['genTime']) || !empty($timestampData['policy']) || !empty($timestampData['serialNumber'])) {
2✔
168
                                $result['timestamp'] = $timestampData;
2✔
169
                        }
170
                } catch (\Throwable $e) {
×
171
                }
172

173
                if (!isset($result['signingTime']) || !$result['signingTime'] instanceof \DateTime) {
2✔
174
                        $result['signingTime'] = $tsa->getSigninTime($decoded);
2✔
175
                }
176
                return $result;
2✔
177
        }
178

179
        private function extractCertificateChain(string $signature): array {
180
                $pkcs7PemSignature = $this->der2pem($signature);
2✔
181
                $pemCertificates = [];
2✔
182

183
                if (!openssl_pkcs7_read($pkcs7PemSignature, $pemCertificates)) {
2✔
184
                        return [];
×
185
                }
186

187
                $chain = [];
2✔
188
                $isLibreSignRootCA = false;
2✔
189
                $certificateEngine = $this->getCertificateEngine();
2✔
190

191
                foreach ($pemCertificates as $index => $pemCertificate) {
2✔
192
                        $parsed = $certificateEngine->parseCertificate($pemCertificate);
2✔
193
                        if ($parsed) {
2✔
194
                                $parsed['signature_validation'] = [
2✔
195
                                        'id' => 1,
2✔
196
                                        'label' => $this->l10n->t('Signature is valid.'),
2✔
197
                                ];
2✔
198
                                if (!$isLibreSignRootCA) {
2✔
199
                                        $isLibreSignRootCA = $this->isLibreSignRootCA($pemCertificate, $parsed);
2✔
200
                                }
201
                                $parsed['isLibreSignRootCA'] = $isLibreSignRootCA;
2✔
202
                                $chain[$index] = $parsed;
2✔
203
                        }
204
                }
205
                if ($isLibreSignRootCA || $this->isLibreSignFile) {
2✔
206
                        foreach ($chain as &$cert) {
×
207
                                $cert['isLibreSignRootCA'] = true;
×
208
                        }
209
                }
210

211
                return $chain;
2✔
212
        }
213

214
        private function isLibreSignRootCA(string $certificate, array $parsed): bool {
215
                $rootCertificatePem = $this->getRootCertificatePem();
2✔
216
                if (empty($rootCertificatePem)) {
2✔
217
                        return false;
2✔
218
                }
219

220
                $rootFingerprint = openssl_x509_fingerprint($rootCertificatePem, 'sha256');
×
221
                $fingerprint = openssl_x509_fingerprint($certificate, 'sha256');
×
222
                if ($rootFingerprint === $fingerprint) {
×
223
                        return true;
×
224
                }
225

226
                return $this->hasLibreSignCaId($parsed);
×
227
        }
228

229
        private function hasLibreSignCaId(array $parsed): bool {
230
                $instanceId = $this->appConfig->getValueString(Application::APP_ID, 'instance_id', '');
×
231
                if (strlen($instanceId) !== 10 || !isset($parsed['subject']['OU'])) {
×
232
                        return false;
×
233
                }
234

235
                $organizationalUnits = $parsed['subject']['OU'];
×
236
                if (is_string($organizationalUnits)) {
×
237
                        $organizationalUnits = [$organizationalUnits];
×
238
                }
239

240
                foreach ($organizationalUnits as $ou) {
×
241
                        $ou = trim($ou);
×
242
                        if ($this->caIdentifierService->isValidCaId($ou, $instanceId)) {
×
243
                                return true;
×
244
                        }
245
                }
246

247
                return false;
×
248
        }
249

250
        private function getRootCertificatePem(): string {
251
                if (!empty($this->rootCertificatePem)) {
2✔
252
                        return $this->rootCertificatePem;
×
253
                }
254
                $configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path');
2✔
255
                if (empty($configPath)
2✔
256
                        || !is_dir($configPath)
2✔
257
                        || !is_readable($configPath . DIRECTORY_SEPARATOR . 'ca.pem')
2✔
258
                ) {
259
                        return '';
2✔
260
                }
261
                $rootCertificatePem = file_get_contents($configPath . DIRECTORY_SEPARATOR . 'ca.pem');
×
262
                if ($rootCertificatePem === false) {
×
263
                        return '';
×
264
                }
265
                $this->rootCertificatePem = $rootCertificatePem;
×
266
                return $this->rootCertificatePem;
×
267
        }
268

269
        private function enrichLeafWithPopplerData($resource, array $result): array {
270
                if (empty($result['chain'])) {
2✔
271
                        return $result;
×
272
                }
273

274
                $popplerOnlyFields = ['field', 'range', 'certificate_validation'];
2✔
275
                if (!isset($result['chain'][0]['subject'])) {
2✔
276
                        return $result;
×
277
                }
278
                $needPoppler = false;
2✔
279
                foreach ($popplerOnlyFields as $field) {
2✔
280
                        if (empty($result['chain'][0][$field])) {
2✔
281
                                $needPoppler = true;
2✔
282
                                break;
2✔
283
                        }
284
                }
285
                if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
2✔
286
                        $needPoppler = true;
×
287
                }
288
                if (!$needPoppler) {
2✔
289
                        return $result;
×
290
                }
291
                $popplerChain = $this->chainFromPoppler($result['chain'][0]['subject'], $resource);
2✔
292
                if (empty($popplerChain)) {
2✔
293
                        return $result;
2✔
294
                }
295
                foreach ($popplerOnlyFields as $field) {
×
296
                        if (isset($popplerChain[$field])) {
×
297
                                $result['chain'][0][$field] = $popplerChain[$field];
×
298
                        }
299
                }
300
                if (!isset($result['chain'][0]['signature_validation']) || $result['chain'][0]['signature_validation']['id'] !== 1) {
×
301
                        if (isset($popplerChain['signature_validation'])) {
×
302
                                $result['chain'][0]['signature_validation'] = $popplerChain['signature_validation'];
×
303
                        }
304
                }
305
                return $result;
×
306
        }
307

308
        private function chainFromPoppler(array $subject, $resource): array {
309
                $fromFallback = $this->popplerUtilsPdfSignFallback($resource);
2✔
310
                foreach ($fromFallback as $popplerSig) {
2✔
311
                        if (!isset($popplerSig['chain'][0]['subject'])) {
2✔
312
                                continue;
×
313
                        }
314
                        if ($popplerSig['chain'][0]['subject'] == $subject) {
2✔
315
                                return $popplerSig['chain'][0];
×
316
                        }
317
                }
318
                return [];
2✔
319
        }
320

321
        private function popplerUtilsPdfSignFallback($resource): array {
322
                if (!empty($this->signaturesFromPoppler)) {
2✔
323
                        return $this->signaturesFromPoppler;
1✔
324
                }
325
                if (shell_exec('which pdfsig') === null) {
1✔
326
                        return $this->signaturesFromPoppler;
×
327
                }
328
                rewind($resource);
1✔
329
                $content = stream_get_contents($resource);
1✔
330
                $tempFile = $this->tempManager->getTemporaryFile('file.pdf');
1✔
331
                file_put_contents($tempFile, $content);
1✔
332

333
                $content = shell_exec('env TZ=UTC pdfsig ' . $tempFile);
1✔
334
                if (empty($content)) {
1✔
335
                        return $this->signaturesFromPoppler;
×
336
                }
337
                $lines = explode("\n", $content);
1✔
338

339
                $lastSignature = 0;
1✔
340
                foreach ($lines as $item) {
1✔
341
                        $isFirstLevel = preg_match('/^Signature\s#(\d)/', $item, $match);
1✔
342
                        if ($isFirstLevel) {
1✔
343
                                $lastSignature = (int)$match[1] - 1;
1✔
344
                                $this->signaturesFromPoppler[$lastSignature] = [];
1✔
345
                                continue;
1✔
346
                        }
347

348
                        $match = [];
1✔
349
                        $isSecondLevel = preg_match('/^\s+-\s(?<key>.+):\s(?<value>.*)/', $item, $match);
1✔
350
                        if ($isSecondLevel) {
1✔
351
                                switch ((string)$match['key']) {
1✔
352
                                        case 'Signing Time':
1✔
353
                                                $this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC'));
1✔
354
                                                break;
1✔
355
                                        case 'Signer full Distinguished Name':
1✔
356
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']);
1✔
357
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['name'] = $match['value'];
1✔
358
                                                break;
1✔
359
                                        case 'Signing Hash Algorithm':
1✔
360
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signatureTypeSN'] = $match['value'];
1✔
361
                                                break;
1✔
362
                                        case 'Signature Validation':
1✔
363
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signature_validation'] = $this->getReadableSigState($match['value']);
1✔
364
                                                break;
1✔
365
                                        case 'Certificate Validation':
1✔
366
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['certificate_validation'] = $this->getReadableCertState($match['value']);
1✔
367
                                                break;
1✔
368
                                        case 'Signed Ranges':
1✔
369
                                                if (preg_match('/\[(\d+) - (\d+)\], \[(\d+) - (\d+)\]/', $match['value'], $ranges)) {
1✔
370
                                                        $this->signaturesFromPoppler[$lastSignature]['chain'][0]['range'] = [
1✔
371
                                                                'offset1' => (int)$ranges[1],
1✔
372
                                                                'length1' => (int)$ranges[2],
1✔
373
                                                                'offset2' => (int)$ranges[3],
1✔
374
                                                                'length2' => (int)$ranges[4],
1✔
375
                                                        ];
1✔
376
                                                }
377
                                                break;
1✔
378
                                        case 'Signature Field Name':
1✔
379
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['field'] = $match['value'];
1✔
380
                                                break;
1✔
381
                                        case 'Signature Validation':
1✔
382
                                        case 'Signature Type':
1✔
383
                                        case 'Total document signed':
1✔
384
                                        case 'Not total document signed':
1✔
385
                                        default:
386
                                                break;
1✔
387
                                }
388
                        }
389
                }
390
                return $this->signaturesFromPoppler;
1✔
391
        }
392

393
        private function getReadableSigState(string $status) {
394
                return match ($status) {
2✔
395
                        'Signature is Valid.' => [
1✔
396
                                'id' => 1,
1✔
397
                                'label' => $this->l10n->t('Signature is valid.'),
1✔
398
                        ],
1✔
399
                        'Signature is Invalid.' => [
×
400
                                'id' => 2,
×
401
                                'label' => $this->l10n->t('Signature is invalid.'),
×
402
                        ],
×
403
                        'Digest Mismatch.' => [
1✔
404
                                'id' => 3,
1✔
405
                                'label' => $this->l10n->t('Digest mismatch.'),
1✔
406
                        ],
1✔
407
                        "Document isn't signed or corrupted data." => [
×
408
                                'id' => 4,
×
409
                                'label' => $this->l10n->t("Document isn't signed or corrupted data."),
×
410
                        ],
×
411
                        'Signature has not yet been verified.' => [
×
412
                                'id' => 5,
×
413
                                'label' => $this->l10n->t('Signature has not yet been verified.'),
×
414
                        ],
×
415
                        default => [
2✔
416
                                'id' => 6,
2✔
417
                                'label' => $this->l10n->t('Unknown validation failure.'),
2✔
418
                        ],
2✔
419
                };
2✔
420
        }
421

422

423
        private function getReadableCertState(string $status) {
424
                return match ($status) {
1✔
425
                        'Certificate is Trusted.' => [
×
426
                                'id' => 1,
×
427
                                'label' => $this->l10n->t('Certificate is trusted.'),
×
428
                        ],
×
429
                        "Certificate issuer isn't Trusted." => [
×
430
                                'id' => 2,
×
431
                                'label' => $this->l10n->t("Certificate issuer isn't trusted."),
×
432
                        ],
×
433
                        'Certificate issuer is unknown.' => [
1✔
434
                                'id' => 3,
1✔
435
                                'label' => $this->l10n->t('Certificate issuer is unknown.'),
1✔
436
                        ],
1✔
437
                        'Certificate has been Revoked.' => [
×
438
                                'id' => 4,
×
439
                                'label' => $this->l10n->t('Certificate has been revoked.'),
×
440
                        ],
×
441
                        'Certificate has Expired' => [
×
442
                                'id' => 5,
×
443
                                'label' => $this->l10n->t('Certificate has expired'),
×
444
                        ],
×
445
                        'Certificate has not yet been verified.' => [
×
446
                                'id' => 6,
×
447
                                'label' => $this->l10n->t('Certificate has not yet been verified.'),
×
448
                        ],
×
449
                        default => [
1✔
450
                                'id' => 7,
1✔
451
                                'label' => $this->l10n->t('Unknown issue with Certificate or corrupted data.')
1✔
452
                        ],
1✔
453
                };
1✔
454
        }
455

456

457
        private function parseDistinguishedNameWithMultipleValues(string $dn): array {
458
                $result = [];
1✔
459
                $pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn);
1✔
460

461
                foreach ($pairs as $pair) {
1✔
462
                        [$key, $value] = explode('=', $pair, 2);
1✔
463
                        if (empty($key) || empty($value)) {
1✔
464
                                return $result;
×
465
                        }
466
                        $key = trim($key);
1✔
467
                        $value = trim($value);
1✔
468
                        $value = trim($value, '"');
1✔
469

470
                        if (!isset($result[$key])) {
1✔
471
                                $result[$key] = $value;
1✔
472
                        } else {
473
                                if (!is_array($result[$key])) {
×
474
                                        $result[$key] = [$result[$key]];
×
475
                                }
476
                                $result[$key][] = $value;
×
477
                        }
478
                }
479

480
                return $result;
1✔
481
        }
482

483
        private function der2pem($derData) {
484
                $pem = chunk_split(base64_encode((string)$derData), 64, "\n");
2✔
485
                $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
2✔
486
                return $pem;
2✔
487
        }
488

489
        private function getHandler(): SignEngineHandler {
490
                $sign_engine = $this->appConfig->getValueString(Application::APP_ID, 'sign_engine', 'JSignPdf');
1✔
491
                $property = lcfirst($sign_engine) . 'Handler';
1✔
492
                if (!property_exists($this, $property)) {
1✔
493
                        throw new LibresignException($this->l10n->t('Invalid Sign engine.'), 400);
×
494
                }
495
                $classHandler = 'OCA\\Libresign\\Handler\\SignEngine\\' . ucfirst($property);
1✔
496
                if (!$this->$property instanceof $classHandler) {
1✔
497
                        $this->$property = \OCP\Server::get($classHandler);
1✔
498
                }
499
                return $this->$property;
1✔
500
        }
501

502
        #[\Override]
503
        public function sign(): File {
504
                $this->beforeSign();
1✔
505

506
                $signedContent = $this->getHandler()
1✔
507
                        ->setCertificate($this->getCertificate())
1✔
508
                        ->setInputFile($this->getInputFile())
1✔
509
                        ->setPassword($this->getPassword())
1✔
510
                        ->setSignatureParams($this->getSignatureParams())
1✔
511
                        ->setVisibleElements($this->getVisibleElements())
1✔
512
                        ->getSignedContent();
1✔
513
                $this->getInputFile()->putContent($signedContent);
×
514
                return $this->getInputFile();
×
515
        }
516

517
        public function isHandlerOk(): bool {
518
                return $this->certificateEngineFactory->getEngine()->isSetupOk();
1✔
519
        }
520
}
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