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

LibreSign / libresign / 19074450473

04 Nov 2025 03:47PM UTC coverage: 39.581%. First build
19074450473

Pull #5731

github

web-flow
Merge 4eab882d2 into b17fffe83
Pull Request #5731: feat: add multiple ou

35 of 112 new or added lines in 10 files covered. (31.25%)

4570 of 11546 relevant lines covered (39.58%)

2.95 hits per line

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

44.8
/lib/Handler/CertificateEngine/AEngineHandler.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\CertificateEngine;
10

11
use OCA\Libresign\AppInfo\Application;
12
use OCA\Libresign\Exception\EmptyCertificateException;
13
use OCA\Libresign\Exception\InvalidPasswordException;
14
use OCA\Libresign\Exception\LibresignException;
15
use OCA\Libresign\Helper\ConfigureCheckHelper;
16
use OCA\Libresign\Helper\MagicGetterSetterTrait;
17
use OCA\Libresign\Service\CertificatePolicyService;
18
use OCP\Files\AppData\IAppDataFactory;
19
use OCP\Files\IAppData;
20
use OCP\Files\SimpleFS\ISimpleFolder;
21
use OCP\IAppConfig;
22
use OCP\IConfig;
23
use OCP\IDateTimeFormatter;
24
use OCP\ITempManager;
25
use OCP\IURLGenerator;
26
use OpenSSLAsymmetricKey;
27
use OpenSSLCertificate;
28
use ReflectionClass;
29

30
/**
31
 * @method IEngineHandler setPassword(string $password)
32
 * @method string getPassword()
33
 * @method IEngineHandler setCommonName(string $commonName)
34
 * @method string getCommonName()
35
 * @method IEngineHandler setHosts(array $hosts)
36
 * @method array getHosts()
37
 * @method IEngineHandler setFriendlyName(string $friendlyName)
38
 * @method string getFriendlyName()
39
 * @method IEngineHandler setCountry(string $country)
40
 * @method string getCountry()
41
 * @method IEngineHandler setState(string $state)
42
 * @method string getState()
43
 * @method IEngineHandler setLocality(string $locality)
44
 * @method string getLocality()
45
 * @method IEngineHandler setOrganization(string $organization)
46
 * @method string getOrganization()
47
 * @method IEngineHandler setOrganizationalUnit(array $organizationalUnit)
48
 * @method array getOrganizationalUnit()
49
 * @method IEngineHandler setUID(string $UID)
50
 * @method string getName()
51
 */
52
abstract class AEngineHandler implements IEngineHandler {
53
        use MagicGetterSetterTrait;
54
        use OrderCertificatesTrait;
55

56
        protected string $commonName = '';
57
        protected array $hosts = [];
58
        protected string $friendlyName = '';
59
        protected string $country = '';
60
        protected string $state = '';
61
        protected string $locality = '';
62
        protected string $organization = '';
63
        protected array $organizationalUnit = [];
64
        protected string $UID = '';
65
        protected string $password = '';
66
        protected string $configPath = '';
67
        protected string $engine = '';
68
        protected string $certificate = '';
69
        protected IAppData $appData;
70

71
        public function __construct(
72
                protected IConfig $config,
73
                protected IAppConfig $appConfig,
74
                protected IAppDataFactory $appDataFactory,
75
                protected IDateTimeFormatter $dateTimeFormatter,
76
                protected ITempManager $tempManager,
77
                protected CertificatePolicyService $certificatePolicyService,
78
                protected IURLGenerator $urlGenerator,
79
        ) {
80
                $this->appData = $appDataFactory->get('libresign');
64✔
81
        }
82

83
        protected function exportToPkcs12(
84
                OpenSSLCertificate|string $certificate,
85
                OpenSSLAsymmetricKey|OpenSSLCertificate|string $privateKey,
86
                array $options = [],
87
        ): string {
88
                if (empty($certificate) || empty($privateKey)) {
7✔
89
                        throw new EmptyCertificateException();
×
90
                }
91
                $certContent = null;
7✔
92
                try {
93
                        openssl_pkcs12_export(
7✔
94
                                $certificate,
7✔
95
                                $certContent,
7✔
96
                                $privateKey,
7✔
97
                                $this->getPassword(),
7✔
98
                                $options,
7✔
99
                        );
7✔
100
                        if (!$certContent) {
7✔
101
                                throw new \Exception();
7✔
102
                        }
103
                } catch (\Throwable) {
×
104
                        throw new LibresignException('Error while creating certificate file', 500);
×
105
                }
106

107
                return $certContent;
7✔
108
        }
109

110
        #[\Override]
111
        public function updatePassword(string $certificate, string $currentPrivateKey, string $newPrivateKey): string {
112
                if (empty($certificate) || empty($currentPrivateKey) || empty($newPrivateKey)) {
×
113
                        throw new EmptyCertificateException();
×
114
                }
115
                $certContent = $this->opensslPkcs12Read($certificate, $currentPrivateKey);
×
116
                $this->setPassword($newPrivateKey);
×
117
                $certContent = self::exportToPkcs12($certContent['cert'], $certContent['pkey']);
×
118
                return $certContent;
×
119
        }
120

121
        #[\Override]
122
        public function readCertificate(string $certificate, string $privateKey): array {
123
                if (empty($certificate) || empty($privateKey)) {
9✔
124
                        throw new EmptyCertificateException();
1✔
125
                }
126
                $certContent = $this->opensslPkcs12Read($certificate, $privateKey);
8✔
127

128
                $return = $this->parseX509($certContent['cert']);
6✔
129
                if (isset($certContent['extracerts'])) {
6✔
130
                        foreach ($certContent['extracerts'] as $extraCert) {
6✔
131
                                $return['extracerts'][] = $this->parseX509($extraCert);
6✔
132
                        }
133
                        $return['extracerts'] = $this->orderCertificates($return['extracerts']);
6✔
134
                }
135
                return $return;
6✔
136
        }
137

138
        private function parseX509(string $x509): array {
139
                $parsed = openssl_x509_parse(openssl_x509_read($x509));
6✔
140

141
                $return = self::convertArrayToUtf8($parsed);
6✔
142

143
                foreach (['subject', 'issuer'] as $actor) {
6✔
144
                        foreach ($return[$actor] as $part => $value) {
6✔
145
                                if (is_string($value) && str_contains($value, ', ')) {
6✔
NEW
146
                                        $return[$actor][$part] = explode(', ', $value);
×
147
                                } else {
148
                                        $return[$actor][$part] = $value;
6✔
149
                                }
150
                        }
151
                }
152

153
                $return['valid_from'] = $this->dateTimeFormatter->formatDateTime($parsed['validFrom_time_t']);
6✔
154
                $return['valid_to'] = $this->dateTimeFormatter->formatDateTime($parsed['validTo_time_t']);
6✔
155
                return $return;
6✔
156
        }
157

158
        private static function convertArrayToUtf8($array) {
159
                foreach ($array as $key => $value) {
6✔
160
                        if (is_array($value)) {
6✔
161
                                $array[$key] = self::convertArrayToUtf8($value);
6✔
162
                        } elseif (is_string($value)) {
6✔
163
                                $array[$key] = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
6✔
164
                        }
165
                }
166
                return $array;
6✔
167
        }
168

169
        public function opensslPkcs12Read(string &$certificate, string $privateKey): array {
170
                openssl_pkcs12_read($certificate, $certContent, $privateKey);
8✔
171
                if (!empty($certContent)) {
8✔
172
                        return $certContent;
6✔
173
                }
174
                /**
175
                 * Reference:
176
                 *
177
                 * https://github.com/php/php-src/issues/12128
178
                 * https://www.php.net/manual/en/function.openssl-pkcs12-read.php#128992
179
                 */
180
                $msg = openssl_error_string();
2✔
181
                if ($msg === 'error:0308010C:digital envelope routines::unsupported') {
2✔
182
                        $tempPassword = $this->tempManager->getTemporaryFile();
×
183
                        $tempEncriptedOriginal = $this->tempManager->getTemporaryFile();
×
184
                        $tempEncriptedRepacked = $this->tempManager->getTemporaryFile();
×
185
                        $tempDecrypted = $this->tempManager->getTemporaryFile();
×
186
                        file_put_contents($tempPassword, $privateKey);
×
187
                        file_put_contents($tempEncriptedOriginal, $certificate);
×
188
                        shell_exec(<<<REPACK_COMMAND
×
189
                                cat $tempPassword | openssl pkcs12 -legacy -in $tempEncriptedOriginal -nodes -out $tempDecrypted -passin stdin &&
×
190
                                cat $tempPassword | openssl pkcs12 -in $tempDecrypted -export -out $tempEncriptedRepacked -passout stdin
×
191
                                REPACK_COMMAND
×
192
                        );
×
193
                        $certificateRepacked = file_get_contents($tempEncriptedRepacked);
×
194
                        openssl_pkcs12_read($certificateRepacked, $certContent, $privateKey);
×
195
                        if (!empty($certContent)) {
×
196
                                $certificate = $certificateRepacked;
×
197
                                return $certContent;
×
198
                        }
199
                }
200
                throw new InvalidPasswordException();
2✔
201
        }
202

203
        /**
204
         * @param (int|string) $name
205
         *
206
         * @psalm-param array-key $name
207
         */
208
        public function translateToLong($name): string {
209
                return match ($name) {
3✔
210
                        'CN' => 'CommonName',
×
211
                        'C' => 'Country',
3✔
212
                        'ST' => 'State',
×
213
                        'L' => 'Locality',
×
214
                        'O' => 'Organization',
3✔
215
                        'OU' => 'OrganizationalUnit',
2✔
216
                        'UID' => 'UserIdentifier',
×
217
                        default => '',
3✔
218
                };
3✔
219
        }
220

221
        public function setEngine(string $engine): void {
222
                $this->appConfig->setValueString(Application::APP_ID, 'certificate_engine', $engine);
×
223
                $this->engine = $engine;
×
224
        }
225

226
        #[\Override]
227
        public function getEngine(): string {
228
                if ($this->engine) {
×
229
                        return $this->engine;
×
230
                }
231
                $this->engine = $this->appConfig->getValueString(Application::APP_ID, 'certificate_engine', 'openssl');
×
232
                return $this->engine;
×
233
        }
234

235
        #[\Override]
236
        public function populateInstance(array $rootCert): IEngineHandler {
237
                if (empty($rootCert)) {
8✔
238
                        $rootCert = $this->appConfig->getValueArray(Application::APP_ID, 'rootCert');
8✔
239
                }
240
                if (!$rootCert) {
8✔
241
                        return $this;
8✔
242
                }
243
                if (!empty($rootCert['names'])) {
×
244
                        foreach ($rootCert['names'] as $id => $customName) {
×
245
                                $longCustomName = $this->translateToLong($id);
×
246
                                // Prevent to save a property that don't exists
247
                                if (!property_exists($this, lcfirst($longCustomName))) {
×
248
                                        continue;
×
249
                                }
250
                                $this->{'set' . ucfirst($longCustomName)}($customName['value']);
×
251
                        }
252
                }
253
                if (!$this->getCommonName()) {
×
254
                        $this->setCommonName($rootCert['commonName']);
×
255
                }
256
                return $this;
×
257
        }
258

259
        #[\Override]
260
        public function getConfigPath(): string {
261
                if ($this->configPath) {
16✔
262
                        return $this->configPath;
15✔
263
                }
264
                $this->configPath = $this->appConfig->getValueString(Application::APP_ID, 'config_path');
16✔
265
                if ($this->configPath
16✔
266
                        && str_ends_with($this->configPath, $this->getName() . '_config')
16✔
267
                        && is_dir($this->configPath)
16✔
268
                ) {
269
                        return $this->configPath;
10✔
270
                }
271
                try {
272
                        $folder = $this->appData->getFolder($this->getName() . '_config');
7✔
273
                        if (!$folder->fileExists('/')) {
7✔
274
                                throw new \Exception();
7✔
275
                        }
276
                } catch (\Throwable) {
×
277
                        $folder = $this->appData->newFolder($this->getName() . '_config');
×
278
                }
279
                $dataDir = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
7✔
280
                $this->configPath = $dataDir . '/' . $this->getInternalPathOfFolder($folder);
7✔
281
                if (!is_dir($this->configPath)) {
7✔
282
                        $currentFile = realpath(__DIR__);
7✔
283
                        $owner = posix_getpwuid(fileowner($currentFile));
7✔
284
                        $fullCommand = 'mkdir -p "' . $this->configPath . '"';
7✔
285
                        if (posix_getuid() !== $owner['uid']) {
7✔
286
                                $fullCommand = 'runuser -u ' . $owner['name'] . ' -- ' . $fullCommand;
×
287
                        }
288
                        exec($fullCommand);
7✔
289
                }
290
                return $this->configPath;
7✔
291
        }
292

293
        /**
294
         * @todo check a best solution to don't use reflection
295
         */
296
        private function getInternalPathOfFolder(ISimpleFolder $node): string {
297
                $reflection = new \ReflectionClass($node);
7✔
298
                $reflectionProperty = $reflection->getProperty('folder');
7✔
299
                $folder = $reflectionProperty->getValue($node);
7✔
300
                $path = $folder->getInternalPath();
7✔
301
                return $path;
7✔
302
        }
303

304
        #[\Override]
305
        public function setConfigPath(string $configPath): IEngineHandler {
306
                if (!$configPath) {
×
307
                        $this->appConfig->deleteKey(Application::APP_ID, 'config_path');
×
308
                } else {
309
                        if (!is_dir($configPath)) {
×
310
                                mkdir(
×
311
                                        directory: $configPath,
×
312
                                        recursive: true,
×
313
                                );
×
314
                        }
315
                        $this->appConfig->setValueString(Application::APP_ID, 'config_path', $configPath);
×
316
                }
317
                $this->configPath = $configPath;
×
318
                return $this;
×
319
        }
320

321
        public function getName(): string {
322
                $reflect = new ReflectionClass($this);
16✔
323
                $className = $reflect->getShortName();
16✔
324
                $name = strtolower(substr($className, 0, -7));
16✔
325
                return $name;
16✔
326
        }
327

328
        protected function getNames(): array {
329
                $names = [
15✔
330
                        'C' => $this->getCountry(),
15✔
331
                        'ST' => $this->getState(),
15✔
332
                        'L' => $this->getLocality(),
15✔
333
                        'O' => $this->getOrganization(),
15✔
334
                        'OU' => $this->getOrganizationalUnit(),
15✔
335
                ];
15✔
336
                if ($uid = $this->getUID()) {
15✔
337
                        $names['UID'] = $uid;
×
338
                }
339
                $names = array_filter($names, fn ($v) => !empty($v));
15✔
340
                return $names;
15✔
341
        }
342

343
        public function getUID(): string {
344
                return str_replace(' ', '+', $this->UID);
15✔
345
        }
346

347
        #[\Override]
348
        public function getLeafExpiryInDays(): int {
349
                $exp = $this->appConfig->getValueInt(Application::APP_ID, 'expiry_in_days', 365);
7✔
350
                return $exp > 0 ? $exp : 365;
7✔
351
        }
352

353
        #[\Override]
354
        public function getCaExpiryInDays(): int {
355
                $exp = $this->appConfig->getValueInt(Application::APP_ID, 'ca_expiry_in_days', 3650); // 10 years
14✔
356
                return $exp > 0 ? $exp : 3650;
14✔
357
        }
358

359
        private function getCertificatePolicy(): array {
360
                $return = ['policySection' => []];
1✔
361
                $oid = $this->certificatePolicyService->getOid();
1✔
362
                $cps = $this->certificatePolicyService->getCps();
1✔
363
                if ($oid && $cps) {
1✔
364
                        $return['policySection'][] = [
×
365
                                'OID' => $oid,
×
366
                                'CPS' => $cps,
×
367
                        ];
×
368
                }
369
                return $return;
1✔
370
        }
371

372
        abstract protected function getConfigureCheckResourceName(): string;
373

374
        abstract protected function getCertificateRegenerationTip(): string;
375

376
        abstract protected function getEngineSpecificChecks(): array;
377

378
        abstract protected function getSetupSuccessMessage(): string;
379

380
        abstract protected function getSetupErrorMessage(): string;
381

382
        abstract protected function getSetupErrorTip(): string;
383

384
        #[\Override]
385
        public function configureCheck(): array {
386
                $checks = $this->getEngineSpecificChecks();
1✔
387

388
                if (!$this->isSetupOk()) {
1✔
389
                        return array_merge($checks, [
1✔
390
                                (new ConfigureCheckHelper())
1✔
391
                                        ->setErrorMessage($this->getSetupErrorMessage())
1✔
392
                                        ->setResource($this->getConfigureCheckResourceName())
1✔
393
                                        ->setTip($this->getSetupErrorTip())
1✔
394
                        ]);
1✔
395
                }
396

397
                $checks[] = (new ConfigureCheckHelper())
×
398
                        ->setSuccessMessage($this->getSetupSuccessMessage())
×
399
                        ->setResource($this->getConfigureCheckResourceName());
×
400

401
                $modernFeaturesCheck = $this->checkRootCertificateModernFeatures();
×
402
                if ($modernFeaturesCheck) {
×
403
                        $checks[] = $modernFeaturesCheck;
×
404
                }
405

406
                return $checks;
×
407
        }
408

409
        protected function checkRootCertificateModernFeatures(): ?ConfigureCheckHelper {
410
                $configPath = $this->getConfigPath();
×
411
                $caCertPath = $configPath . DIRECTORY_SEPARATOR . 'ca.pem';
×
412

413
                try {
414
                        $certContent = file_get_contents($caCertPath);
×
415
                        if (!$certContent) {
×
416
                                return (new ConfigureCheckHelper())
×
417
                                        ->setErrorMessage('Failed to read root certificate file')
×
418
                                        ->setResource($this->getConfigureCheckResourceName())
×
419
                                        ->setTip('Check file permissions and disk space');
×
420
                        }
421

422
                        $x509Resource = openssl_x509_read($certContent);
×
423
                        if (!$x509Resource) {
×
424
                                return (new ConfigureCheckHelper())
×
425
                                        ->setErrorMessage('Failed to parse root certificate')
×
426
                                        ->setResource($this->getConfigureCheckResourceName())
×
427
                                        ->setTip('Root certificate file may be corrupted or invalid');
×
428
                        }
429

430
                        $parsed = openssl_x509_parse($x509Resource);
×
431
                        if (!$parsed) {
×
432
                                return (new ConfigureCheckHelper())
×
433
                                        ->setErrorMessage('Failed to extract root certificate information')
×
434
                                        ->setResource($this->getConfigureCheckResourceName())
×
435
                                        ->setTip('Root certificate may be in an unsupported format');
×
436
                        }
437

438
                        $criticalIssues = [];
×
439
                        $minorIssues = [];
×
440

441
                        if (isset($parsed['serialNumber'])) {
×
442
                                $serialNumber = $parsed['serialNumber'];
×
443
                                $serialDecimal = hexdec($serialNumber);
×
444
                                if ($serialDecimal <= 1) {
×
445
                                        $minorIssues[] = 'Serial number is simple (zero or one)';
×
446
                                }
447
                        } else {
448
                                $criticalIssues[] = 'Serial number is missing';
×
449
                        }
450

451
                        $missingExtensions = [];
×
452
                        if (!isset($parsed['extensions']['subjectKeyIdentifier'])) {
×
453
                                $missingExtensions[] = 'Subject Key Identifier (SKI)';
×
454
                        }
455

456
                        $isSelfSigned = (isset($parsed['issuer']) && isset($parsed['subject'])
×
457
                                                        && $parsed['issuer'] === $parsed['subject']);
×
458

459
                        /**
460
                         * @todo workarround for missing AKI at certificates generated by CFSSL.
461
                         *
462
                         * CFSSL does not add Authority Key Identifier (AKI) to self-signed root certificates.
463
                         */
464
                        if (!$isSelfSigned && !isset($parsed['extensions']['authorityKeyIdentifier'])) {
×
465
                                $missingExtensions[] = 'Authority Key Identifier (AKI)';
×
466
                        }
467

468
                        if (!isset($parsed['extensions']['crlDistributionPoints'])) {
×
469
                                $missingExtensions[] = 'CRL Distribution Points';
×
470
                        }
471

472
                        if (!empty($missingExtensions)) {
×
473
                                $extensionsList = implode(', ', $missingExtensions);
×
474
                                $minorIssues[] = "Missing modern extensions: {$extensionsList}";
×
475
                        }
476

NEW
477
                        $hasLibresignCaUuid = $this->validateLibresignCaUuidInCertificate($parsed);
×
NEW
478
                        if (!$hasLibresignCaUuid) {
×
NEW
479
                                $minorIssues[] = 'LibreSign CA UUID not found in Organizational Unit';
×
480
                        }
481

482
                        if (!empty($criticalIssues)) {
×
483
                                $issuesList = implode(', ', $criticalIssues);
×
484
                                return (new ConfigureCheckHelper())
×
485
                                        ->setErrorMessage("Root certificate has critical issues: {$issuesList}")
×
486
                                        ->setResource($this->getConfigureCheckResourceName())
×
487
                                        ->setTip($this->getCertificateRegenerationTip());
×
488
                        }
489

490
                        if (!empty($minorIssues)) {
×
491
                                $issuesList = implode(', ', $minorIssues);
×
492
                                return (new ConfigureCheckHelper())
×
493
                                        ->setInfoMessage("Root certificate could benefit from modern features: {$issuesList}")
×
494
                                        ->setResource($this->getConfigureCheckResourceName())
×
495
                                        ->setTip($this->getCertificateRegenerationTip() . ' (recommended but not required)');
×
496
                        }
497

498
                        return null;
×
499

500
                } catch (\Exception $e) {
×
501
                        return (new ConfigureCheckHelper())
×
502
                                ->setErrorMessage('Failed to analyze root certificate: ' . $e->getMessage())
×
503
                                ->setResource($this->getConfigureCheckResourceName())
×
504
                                ->setTip('Check if the root certificate file is valid');
×
505
                }
506
        }
507

508
        private function validateLibresignCaUuidInCertificate(array $parsed): bool {
NEW
509
                if (!isset($parsed['subject']['OU'])) {
×
NEW
510
                        return false;
×
511
                }
512

NEW
513
                $instanceId = $this->getInstanceId();
×
NEW
514
                if (empty($instanceId)) {
×
NEW
515
                        return false;
×
516
                }
517

NEW
518
                $organizationalUnits = $parsed['subject']['OU'];
×
519

NEW
520
                if (is_string($organizationalUnits)) {
×
NEW
521
                        if (str_contains($organizationalUnits, ', ')) {
×
NEW
522
                                $organizationalUnits = explode(', ', $organizationalUnits);
×
523
                        } else {
NEW
524
                                $organizationalUnits = [$organizationalUnits];
×
525
                        }
526
                }
527

NEW
528
                $expectedCaUuid = 'libresign-ca-id:' . $instanceId;
×
529

NEW
530
                foreach ($organizationalUnits as $ou) {
×
NEW
531
                        if (trim($ou) === $expectedCaUuid) {
×
NEW
532
                                return true;
×
533
                        }
534
                }
535

NEW
536
                return false;
×
537
        }
538

539
        private function getInstanceId(): string {
NEW
540
                $instanceId = $this->appConfig->getValueString(Application::APP_ID, 'instance_id', '');
×
NEW
541
                if (strlen($instanceId) === 10) {
×
NEW
542
                        return $instanceId;
×
543
                }
NEW
544
                return '';
×
545
        }
546

547
        #[\Override]
548
        public function toArray(): array {
549
                $return = [
1✔
550
                        'configPath' => $this->getConfigPath(),
1✔
551
                        'generated' => $this->isSetupOk(),
1✔
552
                        'rootCert' => [
1✔
553
                                'commonName' => $this->getCommonName(),
1✔
554
                                'names' => [],
1✔
555
                        ],
1✔
556
                ];
1✔
557
                $return = array_merge(
1✔
558
                        $return,
1✔
559
                        $this->getCertificatePolicy(),
1✔
560
                );
1✔
561
                $names = $this->getNames();
1✔
562
                foreach ($names as $name => $value) {
1✔
563
                        $return['rootCert']['names'][] = [
×
564
                                'id' => $name,
×
565
                                'value' => $value,
×
566
                        ];
×
567
                }
568
                return $return;
1✔
569
        }
570

571
        protected function getCrlDistributionUrl(): string {
572
                return $this->urlGenerator->linkToRouteAbsolute('libresign.crl.getRevocationList');
14✔
573
        }
574
}
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