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

LibreSign / libresign / 18896655289

29 Oct 2025 04:00AM UTC coverage: 39.424%. First build
18896655289

Pull #5697

github

web-flow
Merge e37cf9354 into 89afa5c9a
Pull Request #5697: fix: valdiate return of poppler

1 of 2 new or added lines in 1 file covered. (50.0%)

4488 of 11384 relevant lines covered (39.42%)

2.92 hits per line

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

76.26
/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
                $signerCounter = 0;
6✔
84

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

92
        private function processSignature($resource, ?string $signature, int $signerCounter): array {
93
                $fromFallback = $this->popplerUtilsPdfSignFallback($resource, $signerCounter);
2✔
94
                $result = $fromFallback ?: [];
2✔
95

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

101
                $decoded = ASN1::decodeBER($signature);
2✔
102
                $result = $this->extractTimestampData($decoded, $result);
2✔
103

104
                $chain = $this->extractCertificateChain($signature);
2✔
105
                if (!empty($chain)) {
2✔
106
                        $result['chain'] = $this->orderCertificates($chain);
2✔
107
                        $this->enrichLeafWithPopplerData($result, $fromFallback);
2✔
108
                }
109
                return $result;
2✔
110
        }
111

112
        private function extractTimestampData(array $decoded, array $result): array {
113
                $tsa = new TSA();
2✔
114

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

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

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

133
                if (!openssl_pkcs7_read($pkcs7PemSignature, $pemCertificates)) {
2✔
134
                        return [];
×
135
                }
136

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

149
                return $chain;
2✔
150
        }
151

152
        private function enrichLeafWithPopplerData(array &$result, array $fromFallback): void {
153
                if (empty($fromFallback['chain'][0]) || empty($result['chain'][0])) {
2✔
154
                        return;
×
155
                }
156

157
                $popplerData = $fromFallback['chain'][0];
2✔
158
                $leafCert = &$result['chain'][0];
2✔
159

160
                $popplerOnlyFields = ['field', 'range', 'certificate_validation'];
2✔
161
                foreach ($popplerOnlyFields as $field) {
2✔
162
                        if (isset($popplerData[$field])) {
2✔
163
                                $leafCert[$field] = $popplerData[$field];
2✔
164
                        }
165
                }
166

167
                if (isset($popplerData['signature_validation'])
2✔
168
                        && (!isset($leafCert['signature_validation']) || $leafCert['signature_validation']['id'] !== 1)) {
2✔
169
                        $leafCert['signature_validation'] = $popplerData['signature_validation'];
×
170
                }
171
        }
172

173
        private function popplerUtilsPdfSignFallback($resource, int $signerCounter): array {
174
                if (shell_exec('which pdfsig') === null) {
2✔
175
                        return [];
×
176
                }
177
                if (!empty($this->signaturesFromPoppler)) {
2✔
178
                        return $this->signaturesFromPoppler[$signerCounter] ?? [];
1✔
179
                }
180
                rewind($resource);
1✔
181
                $content = stream_get_contents($resource);
1✔
182
                $tempFile = $this->tempManager->getTemporaryFile('file.pdf');
1✔
183
                file_put_contents($tempFile, $content);
1✔
184

185
                $content = shell_exec('env TZ=UTC pdfsig ' . $tempFile);
1✔
186
                if (empty($content)) {
1✔
187
                        return [];
×
188
                }
189
                $lines = explode("\n", $content);
1✔
190

191
                $lastSignature = 0;
1✔
192
                foreach ($lines as $item) {
1✔
193
                        $isFirstLevel = preg_match('/^Signature\s#(\d)/', $item, $match);
1✔
194
                        if ($isFirstLevel) {
1✔
195
                                $lastSignature = (int)$match[1] - 1;
1✔
196
                                $this->signaturesFromPoppler[$lastSignature] = [];
1✔
197
                                continue;
1✔
198
                        }
199

200
                        $match = [];
1✔
201
                        $isSecondLevel = preg_match('/^\s+-\s(?<key>.+):\s(?<value>.*)/', $item, $match);
1✔
202
                        if ($isSecondLevel) {
1✔
203
                                switch ((string)$match['key']) {
1✔
204
                                        case 'Signing Time':
1✔
205
                                                $this->signaturesFromPoppler[$lastSignature]['signingTime'] = DateTime::createFromFormat('M d Y H:i:s', $match['value'], new \DateTimeZone('UTC'));
1✔
206
                                                break;
1✔
207
                                        case 'Signer full Distinguished Name':
1✔
208
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['subject'] = $this->parseDistinguishedNameWithMultipleValues($match['value']);
1✔
209
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['name'] = $match['value'];
1✔
210
                                                break;
1✔
211
                                        case 'Signing Hash Algorithm':
1✔
212
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signatureTypeSN'] = $match['value'];
1✔
213
                                                break;
1✔
214
                                        case 'Signature Validation':
1✔
215
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['signature_validation'] = $this->getReadableSigState($match['value']);
1✔
216
                                                break;
1✔
217
                                        case 'Certificate Validation':
1✔
218
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['certificate_validation'] = $this->getReadableCertState($match['value']);
1✔
219
                                                break;
1✔
220
                                        case 'Signed Ranges':
1✔
221
                                                preg_match('/\[(\d+) - (\d+)\], \[(\d+) - (\d+)\]/', $match['value'], $ranges);
1✔
222
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['range'] = [
1✔
223
                                                        'offset1' => (int)$ranges[1],
1✔
224
                                                        'length1' => (int)$ranges[2],
1✔
225
                                                        'offset2' => (int)$ranges[3],
1✔
226
                                                        'length2' => (int)$ranges[4],
1✔
227
                                                ];
1✔
228
                                                break;
1✔
229
                                        case 'Signature Field Name':
1✔
230
                                                $this->signaturesFromPoppler[$lastSignature]['chain'][0]['field'] = $match['value'];
1✔
231
                                                break;
1✔
232
                                        case 'Signature Validation':
1✔
233
                                        case 'Signature Type':
1✔
234
                                        case 'Total document signed':
1✔
235
                                        case 'Not total document signed':
1✔
236
                                        default:
237
                                                break;
1✔
238
                                }
239
                        }
240
                }
241
                return $this->signaturesFromPoppler[$signerCounter] ?? [];
1✔
242
        }
243

244
        private function getReadableSigState(string $status) {
245
                return match ($status) {
1✔
246
                        'Signature is Valid.' => [
1✔
247
                                'id' => 1,
1✔
248
                                'label' => $this->l10n->t('Signature is valid.'),
1✔
249
                        ],
1✔
250
                        'Signature is Invalid.' => [
×
251
                                'id' => 2,
×
252
                                'label' => $this->l10n->t('Signature is invalid.'),
×
253
                        ],
×
254
                        'Digest Mismatch.' => [
×
255
                                'id' => 3,
×
256
                                'label' => $this->l10n->t('Digest mismatch.'),
×
257
                        ],
×
258
                        "Document isn't signed or corrupted data." => [
×
259
                                'id' => 4,
×
260
                                'label' => $this->l10n->t("Document isn't signed or corrupted data."),
×
261
                        ],
×
262
                        'Signature has not yet been verified.' => [
×
263
                                'id' => 5,
×
264
                                'label' => $this->l10n->t('Signature has not yet been verified.'),
×
265
                        ],
×
266
                        default => [
1✔
267
                                'id' => 6,
1✔
268
                                'label' => $this->l10n->t('Unknown validation failure.'),
1✔
269
                        ],
1✔
270
                };
1✔
271
        }
272

273

274
        private function getReadableCertState(string $status) {
275
                return match ($status) {
1✔
276
                        'Certificate is Trusted.' => [
×
277
                                'id' => 1,
×
278
                                'label' => $this->l10n->t('Certificate is trusted.'),
×
279
                        ],
×
280
                        "Certificate issuer isn't Trusted." => [
×
281
                                'id' => 2,
×
282
                                'label' => $this->l10n->t("Certificate issuer isn't trusted."),
×
283
                        ],
×
284
                        'Certificate issuer is unknown.' => [
1✔
285
                                'id' => 3,
1✔
286
                                'label' => $this->l10n->t('Certificate issuer is unknown.'),
1✔
287
                        ],
1✔
288
                        'Certificate has been Revoked.' => [
×
289
                                'id' => 4,
×
290
                                'label' => $this->l10n->t('Certificate has been revoked.'),
×
291
                        ],
×
292
                        'Certificate has Expired' => [
×
293
                                'id' => 5,
×
294
                                'label' => $this->l10n->t('Certificate has expired'),
×
295
                        ],
×
296
                        'Certificate has not yet been verified.' => [
×
297
                                'id' => 6,
×
298
                                'label' => $this->l10n->t('Certificate has not yet been verified.'),
×
299
                        ],
×
300
                        default => [
1✔
301
                                'id' => 7,
1✔
302
                                'label' => $this->l10n->t('Unknown issue with Certificate or corrupted data.')
1✔
303
                        ],
1✔
304
                };
1✔
305
        }
306

307

308
        private function parseDistinguishedNameWithMultipleValues(string $dn): array {
309
                $result = [];
1✔
310
                $pairs = preg_split('/,(?=(?:[^"]*"[^"]*")*[^"]*$)/', $dn);
1✔
311

312
                foreach ($pairs as $pair) {
1✔
313
                        [$key, $value] = explode('=', $pair, 2);
1✔
314
                        if (empty($key) || empty($value)) {
1✔
NEW
315
                                throw new LibresignException($this->l10n->t('Invalid Distinguished Name format. Identified value: %s.', [$pair]));
×
316
                        }
317
                        $key = trim($key);
1✔
318
                        $value = trim($value);
1✔
319
                        $value = trim($value, '"');
1✔
320

321
                        if (!isset($result[$key])) {
1✔
322
                                $result[$key] = $value;
1✔
323
                        } else {
324
                                if (!is_array($result[$key])) {
×
325
                                        $result[$key] = [$result[$key]];
×
326
                                }
327
                                $result[$key][] = $value;
×
328
                        }
329
                }
330

331
                return $result;
1✔
332
        }
333

334
        private function der2pem($derData) {
335
                $pem = chunk_split(base64_encode((string)$derData), 64, "\n");
2✔
336
                $pem = "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
2✔
337
                return $pem;
2✔
338
        }
339

340
        private function getHandler(): SignEngineHandler {
341
                $sign_engine = $this->appConfig->getValueString(Application::APP_ID, 'sign_engine', 'JSignPdf');
1✔
342
                $property = lcfirst($sign_engine) . 'Handler';
1✔
343
                if (!property_exists($this, $property)) {
1✔
344
                        throw new LibresignException($this->l10n->t('Invalid Sign engine.'), 400);
×
345
                }
346
                $classHandler = 'OCA\\Libresign\\Handler\\SignEngine\\' . ucfirst($property);
1✔
347
                if (!$this->$property instanceof $classHandler) {
1✔
348
                        $this->$property = \OCP\Server::get($classHandler);
1✔
349
                }
350
                return $this->$property;
1✔
351
        }
352

353
        #[\Override]
354
        public function sign(): File {
355
                $signedContent = $this->getHandler()
1✔
356
                        ->setCertificate($this->getCertificate())
1✔
357
                        ->setInputFile($this->getInputFile())
1✔
358
                        ->setPassword($this->getPassword())
1✔
359
                        ->setSignatureParams($this->getSignatureParams())
1✔
360
                        ->setVisibleElements($this->getVisibleElements())
1✔
361
                        ->getSignedContent();
1✔
362
                $this->getInputFile()->putContent($signedContent);
×
363
                return $this->getInputFile();
×
364
        }
365

366
        public function isHandlerOk(): bool {
367
                return $this->certificateEngineFactory->getEngine()->isSetupOk();
1✔
368
        }
369
}
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

© 2025 Coveralls, Inc