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

LibreSign / libresign / 24160618980

08 Apr 2026 09:52PM UTC coverage: 56.325%. First build
24160618980

Pull #7483

github

web-flow
Merge 37610cb4b into 2c799643d
Pull Request #7483: refactor: replace ps-based process handling with process manager

172 of 309 new or added lines in 7 files covered. (55.66%)

10473 of 18594 relevant lines covered (56.32%)

6.61 hits per line

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

24.0
/lib/Handler/CertificateEngine/CfsslHandler.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 GuzzleHttp\Client;
12
use GuzzleHttp\Exception\ConnectException;
13
use GuzzleHttp\Exception\RequestException;
14
use OCA\Libresign\AppInfo\Application;
15
use OCA\Libresign\Db\CrlMapper;
16
use OCA\Libresign\Enum\CertificateType;
17
use OCA\Libresign\Exception\EmptyCertificateException;
18
use OCA\Libresign\Exception\LibresignException;
19
use OCA\Libresign\Handler\CfsslServerHandler;
20
use OCA\Libresign\Helper\ConfigureCheckHelper;
21
use OCA\Libresign\Service\CaIdentifierService;
22
use OCA\Libresign\Service\CertificatePolicyService;
23
use OCA\Libresign\Service\Crl\CrlRevocationChecker;
24
use OCA\Libresign\Service\Install\InstallService;
25
use OCA\Libresign\Service\Process\ProcessManager;
26
use OCA\Libresign\Vendor\Symfony\Component\Process\Process;
27
use OCP\Files\AppData\IAppDataFactory;
28
use OCP\IAppConfig;
29
use OCP\IConfig;
30
use OCP\IDateTimeFormatter;
31
use OCP\ITempManager;
32
use OCP\IURLGenerator;
33
use Psr\Log\LoggerInterface;
34

35
/**
36
 * Class CfsslHandler
37
 *
38
 * @package OCA\Libresign\Handler
39
 *
40
 * @method CfsslHandler setClient(Client $client)
41
 */
42
class CfsslHandler extends AEngineHandler implements IEngineHandler {
43
        public const CFSSL_URI = 'http://127.0.0.1:8888/api/v1/cfssl/';
44
        private const PROCESS_SOURCE = 'cfssl';
45

46
        /** @var Client */
47
        protected $client;
48
        protected $cfsslUri;
49
        private string $binary = '';
50

51
        public function __construct(
52
                protected IConfig $config,
53
                protected IAppConfig $appConfig,
54
                protected IAppDataFactory $appDataFactory,
55
                protected IDateTimeFormatter $dateTimeFormatter,
56
                protected ITempManager $tempManager,
57
                protected CfsslServerHandler $cfsslServerHandler,
58
                protected CertificatePolicyService $certificatePolicyService,
59
                protected IURLGenerator $urlGenerator,
60
                protected CaIdentifierService $caIdentifierService,
61
                protected CrlMapper $crlMapper,
62
                protected LoggerInterface $logger,
63
                CrlRevocationChecker $crlRevocationChecker,
64
                private ProcessManager $processManager,
65
        ) {
66
                parent::__construct(
75✔
67
                        $config,
75✔
68
                        $appConfig,
75✔
69
                        $appDataFactory,
75✔
70
                        $dateTimeFormatter,
75✔
71
                        $tempManager,
75✔
72
                        $certificatePolicyService,
75✔
73
                        $urlGenerator,
75✔
74
                        $caIdentifierService,
75✔
75
                        $logger,
75✔
76
                        $crlRevocationChecker,
75✔
77
                );
75✔
78

79
                $this->cfsslServerHandler->configCallback(fn () => $this->getCurrentConfigPath());
75✔
80
        }
81

82
        #[\Override]
83
        public function generateRootCert(
84
                string $commonName,
85
                array $names = [],
86
        ): void {
87
                if (empty($commonName)) {
×
88
                        throw new EmptyCertificateException('Common Name (CN) cannot be empty for root certificate');
×
89
                }
90

91
                $this->cfsslServerHandler->createConfigServer(
×
92
                        $commonName,
×
93
                        $names,
×
94
                        $this->getCaExpiryInDays(),
×
95
                        $this->getCrlDistributionUrl(),
×
96
                );
×
97

98
                $this->gencert();
×
99

100
                $this->stopIfRunning();
×
101

102
                for ($i = 1; $i <= 4; $i++) {
×
103
                        if ($this->isUp()) {
×
104
                                break;
×
105
                        }
106
                        sleep(2);
×
107
                }
108
        }
109

110
        #[\Override]
111
        public function generateCertificate(): string {
112
                $this->validateRootCertificate();
×
113

114
                $certKeys = $this->newCert();
×
115
                $pkcs12 = parent::exportToPkcs12(
×
116
                        $certKeys['certificate'],
×
117
                        $certKeys['private_key'],
×
118
                        [
×
119
                                'friendly_name' => $this->getFriendlyName(),
×
120
                                'extracerts' => [
×
121
                                        $certKeys['certificate'],
×
122
                                        $certKeys['certificate_request'],
×
123
                                ],
×
124
                        ],
×
125
                );
×
126

127
                $parsed = $this->readCertificate($pkcs12, $this->getPassword());
×
128
                $this->persistSerialNumberToCrl($parsed);
×
129

130
                return $pkcs12;
×
131
        }
132

133
        #[\Override]
134
        public function isSetupOk(): bool {
135
                $configPath = $this->getCurrentConfigPath();
×
136
                $certificate = file_exists($configPath . DIRECTORY_SEPARATOR . 'ca.pem');
×
137
                $privateKey = file_exists($configPath . DIRECTORY_SEPARATOR . 'ca-key.pem');
×
138
                $csrServer = file_exists($configPath . DIRECTORY_SEPARATOR . 'csr_server.json');
×
139
                $configServer = file_exists($configPath . DIRECTORY_SEPARATOR . 'config_server.json');
×
140
                if (!$certificate || !$privateKey || !$csrServer || !$configServer) {
×
141
                        return false;
×
142
                }
143
                try {
144
                        $this->getClient();
×
145
                        return true;
×
146
                } catch (\Throwable) {
×
147
                }
148
                return false;
×
149
        }
150

151
        #[\Override]
152
        protected function getConfigureCheckResourceName(): string {
153
                return 'cfssl-configure';
×
154
        }
155

156
        #[\Override]
157
        protected function getCertificateRegenerationTip(): string {
158
                return 'Consider regenerating the root certificate with: occ libresign:configure:cfssl --cn="Your CA Name"';
×
159
        }
160

161
        #[\Override]
162
        protected function getEngineSpecificChecks(): array {
163
                return $this->checkBinaries();
×
164
        }
165

166
        #[\Override]
167
        protected function getSetupSuccessMessage(): string {
168
                return 'Root certificate config files found.';
×
169
        }
170

171
        #[\Override]
172
        protected function getSetupErrorMessage(): string {
173
                return 'CFSSL (root certificate) not configured.';
×
174
        }
175

176
        #[\Override]
177
        protected function getSetupErrorTip(): string {
178
                return 'Run occ libresign:configure:cfssl --help';
×
179
        }
180

181
        #[\Override]
182
        public function toArray(): array {
183
                $return = parent::toArray();
×
184
                if (!empty($return['configPath'])) {
×
185
                        $return['cfsslUri'] = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_uri');
×
186
                }
187
                return $return;
×
188
        }
189

190
        public function getCommonName(): string {
191
                $uid = $this->getUID();
×
192
                if (!$uid) {
×
193
                        return $this->commonName;
×
194
                }
195
                return $uid . ', ' . $this->commonName;
×
196
        }
197

198
        private function newCert(): array {
199
                $json = [
×
200
                        'json' => [
×
201
                                'profile' => 'client',
×
202
                                'request' => [
×
203
                                        'hosts' => $this->getHosts(),
×
204
                                        'CN' => $this->getCommonName(),
×
205
                                        'key' => [
×
206
                                                'algo' => 'rsa',
×
207
                                                'size' => 2048,
×
208
                                        ],
×
209
                                        'names' => [],
×
210
                                        'crl_url' => $this->getCrlDistributionUrl(),
×
211
                                ],
×
212
                        ],
×
213
                ];
×
214

215
                $names = $this->getNames();
×
216
                foreach ($names as $key => $value) {
×
217
                        if (!empty($value) && is_array($value)) {
×
218
                                $names[$key] = implode(', ', $value);
×
219
                        }
220
                }
221
                if (!empty($names)) {
×
222
                        $json['json']['request']['names'][] = $names;
×
223
                }
224

225
                try {
226
                        $response = $this->getClient()
×
227
                                ->request('post',
×
228
                                        'newcert',
×
229
                                        $json
×
230
                                )
×
231
                        ;
×
232
                } catch (RequestException|ConnectException $th) {
×
233
                        if ($th->getHandlerContext() && $th->getHandlerContext()['error']) {
×
234
                                throw new \Exception($th->getHandlerContext()['error'], 1);
×
235
                        }
236
                        throw new LibresignException($th->getMessage(), 500);
×
237
                }
238

239
                $responseDecoded = json_decode((string)$response->getBody(), true);
×
240
                if (!isset($responseDecoded['success']) || !$responseDecoded['success']) {
×
241
                        throw new LibresignException('Error while generating certificate keys!', 500);
×
242
                }
243

244
                return $responseDecoded['result'];
×
245
        }
246

247
        private function gencert(): void {
248
                $binary = $this->getBinary();
×
249
                $configPath = $this->getCurrentConfigPath();
×
250
                $csrFile = $configPath . '/csr_server.json';
×
251

NEW
252
                $process = $this->createProcess([$binary, 'gencert', '-initca', $csrFile]);
×
NEW
253
                $process->run();
×
NEW
254
                $output = $process->getOutput();
×
255

NEW
256
                if (!$process->isSuccessful() || $output === '') {
×
257
                        throw new \RuntimeException('cfssl without output.');
×
258
                }
259

260
                $json = json_decode($output, true);
×
261
                if (!$json || !isset($json['cert'], $json['key'], $json['csr'])) {
×
262
                        throw new \RuntimeException('Error generating CA: invalid cfssl output.');
×
263
                }
264

265
                file_put_contents($configPath . '/ca.pem', $json['cert']);
×
266
                file_put_contents($configPath . '/ca-key.pem', $json['key']);
×
267
                file_put_contents($configPath . '/ca.csr', $json['csr']);
×
268

269
                $this->persistRootCertificateFromData($json['cert']);
×
270
        }
271

272
        private function getClient(): Client {
273
                if (!$this->client) {
×
274
                        $this->setClient(new Client(['base_uri' => $this->getCfsslUri()]));
×
275
                }
276
                $this->wakeUp();
×
277
                return $this->client;
×
278
        }
279

280
        private function isUp(): bool {
281
                try {
282
                        $client = $this->getClient();
×
283
                        if (!$this->portOpen()) {
×
284
                                throw new LibresignException('CFSSL server is down', 500);
×
285
                        }
286
                        $response = $client
×
287
                                ->request('get',
×
288
                                        'health',
×
289
                                        [
×
290
                                                'base_uri' => $this->getCfsslUri()
×
291
                                        ]
×
292
                                )
×
293
                        ;
×
294
                } catch (RequestException|ConnectException $th) {
×
295
                        switch ($th->getCode()) {
×
296
                                case 404:
×
297
                                        throw new \Exception('Endpoint /health of CFSSL server not found. Maybe you are using incompatible version of CFSSL server. Use latests version.', 1);
×
298
                                default:
299
                                        if ($th->getHandlerContext() && $th->getHandlerContext()['error']) {
×
300
                                                throw new \Exception($th->getHandlerContext()['error'], 1);
×
301
                                        }
302
                                        throw new LibresignException($th->getMessage(), 500);
×
303
                        }
304
                }
305

306
                $responseDecoded = json_decode((string)$response->getBody(), true);
×
307
                if (!isset($responseDecoded['success']) || !$responseDecoded['success']) {
×
308
                        throw new LibresignException('Error while check cfssl API health!', 500);
×
309
                }
310

311
                if (empty($responseDecoded['result']) || empty($responseDecoded['result']['healthy'])) {
×
312
                        return false;
×
313
                }
314

315
                return (bool)$responseDecoded['result']['healthy'];
×
316
        }
317

318
        private function wakeUp(): void {
319
                if ($this->portOpen()) {
×
320
                        return;
×
321
                }
322
                $binary = $this->getBinary();
×
323
                $configPath = $this->getCurrentConfigPath();
×
324
                if (!$configPath) {
×
325
                        throw new LibresignException('CFSSL not configured.');
×
326
                }
327
                $this->cfsslServerHandler->updateExpirity($this->getCaExpiryInDays());
×
NEW
328
                $process = $this->createProcess([
×
NEW
329
                        $binary,
×
NEW
330
                        'serve',
×
NEW
331
                        '-address=127.0.0.1',
×
NEW
332
                        '-ca-key', $configPath . DIRECTORY_SEPARATOR . 'ca-key.pem',
×
NEW
333
                        '-ca', $configPath . DIRECTORY_SEPARATOR . 'ca.pem',
×
NEW
334
                        '-config', $configPath . DIRECTORY_SEPARATOR . 'config_server.json',
×
NEW
335
                ]);
×
NEW
336
                $process->setOptions(['create_new_console' => true]);
×
NEW
337
                $process->setTimeout(null);
×
NEW
338
                $process->disableOutput();
×
NEW
339
                $process->start();
×
340

NEW
341
                $pid = (int)($process->getPid() ?? 0);
×
NEW
342
                if ($pid > 0) {
×
NEW
343
                        $this->processManager->register(self::PROCESS_SOURCE, $pid, [
×
NEW
344
                                'uri' => $this->getCfsslUri(),
×
NEW
345
                        ]);
×
346
                }
347

348
                $loops = 0;
×
349
                while (!$this->portOpen() && $loops <= 4) {
×
350
                        sleep(1);
×
351
                        $loops++;
×
352
                }
353
        }
354

355
        private function portOpen(): bool {
356
                $host = parse_url($this->getCfsslUri(), PHP_URL_HOST);
×
357
                $port = parse_url($this->getCfsslUri(), PHP_URL_PORT);
×
358

359
                set_error_handler(function (int $errno, string $errstr, string $errfile = '', int $errline = 0, array $errcontext = []): bool {
×
360
                        return true;
×
361
                });
×
362
                $socket = fsockopen($host, $port, $errno, $errstr, 0.1);
×
363
                restore_error_handler();
×
364
                if (!$socket || $errno || $errstr) {
×
365
                        return false;
×
366
                }
367
                fclose($socket);
×
368
                return true;
×
369
        }
370

371
        private function getServerPid(): int {
372
                $uri = $this->getCfsslUri();
1✔
373
                return $this->processManager->findRunningPid(
1✔
374
                        self::PROCESS_SOURCE,
1✔
375
                        fn (array $entry): bool => ($entry['context']['uri'] ?? '') === $uri,
1✔
376
                );
1✔
377
        }
378

379
        private function stopIfRunning(): void {
380
                $uri = $this->getCfsslUri();
1✔
381
                $port = (int)(parse_url($uri, PHP_URL_PORT) ?? 0);
1✔
382
                $this->processManager->setSourceHint(self::PROCESS_SOURCE, [
1✔
383
                        'uri' => $uri,
1✔
384
                        'port' => $port,
1✔
385
                ]);
1✔
386

387
                $this->processManager->findRunningPid(
1✔
388
                        self::PROCESS_SOURCE,
1✔
389
                        fn (array $entry): bool => ($entry['context']['uri'] ?? '') === $uri,
1✔
390
                );
1✔
391

392
                foreach ($this->processManager->listRunning(self::PROCESS_SOURCE) as $entry) {
1✔
393
                        if (($entry['context']['uri'] ?? '') !== $uri) {
1✔
NEW
394
                                continue;
×
395
                        }
396

397
                        $pid = (int)($entry['pid'] ?? 0);
1✔
398
                        if ($pid <= 0) {
1✔
NEW
399
                                continue;
×
400
                        }
401
                        $this->processManager->stopPid($pid, SIGKILL);
1✔
402
                        $this->processManager->unregister(self::PROCESS_SOURCE, $pid);
1✔
403
                }
404
        }
405

406
        private function getBinary(): string {
407
                if ($this->binary) {
×
408
                        return $this->binary;
×
409
                }
410

411
                if (PHP_OS_FAMILY === 'Windows') {
×
412
                        throw new LibresignException('Incompatible with Windows');
×
413
                }
414

415
                if ($this->appConfig->hasKey(Application::APP_ID, 'cfssl_bin')) {
×
416
                        $binary = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
×
417
                        if (!file_exists($binary)) {
×
418
                                $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_bin');
×
419
                        }
420
                        return $binary;
×
421
                }
422
                throw new LibresignException('Binary of CFSSL not found. Install binaries.');
×
423
        }
424

425
        private function getCfsslUri(): string {
426
                if ($this->cfsslUri) {
2✔
427
                        return $this->cfsslUri;
×
428
                }
429

430
                if ($uri = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_uri')) {
2✔
431
                        return $uri;
×
432
                }
433
                $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_uri');
2✔
434

435
                $this->cfsslUri = self::CFSSL_URI;
2✔
436
                return $this->cfsslUri;
2✔
437
        }
438

439
        public function setCfsslUri($uri): void {
440
                if ($uri) {
×
441
                        $this->appConfig->setValueString(Application::APP_ID, 'cfssl_uri', $uri);
×
442
                } else {
443
                        $this->appConfig->deleteKey(Application::APP_ID, 'cfssl_uri');
×
444
                }
445
                $this->cfsslUri = $uri;
×
446
        }
447

448
        private function checkBinaries(): array {
449
                if (PHP_OS_FAMILY === 'Windows') {
4✔
450
                        return [
×
451
                                (new ConfigureCheckHelper())
×
452
                                        ->setErrorMessage('CFSSL is incompatible with Windows')
×
453
                                        ->setResource('cfssl'),
×
454
                        ];
×
455
                }
456
                $binary = $this->appConfig->getValueString(Application::APP_ID, 'cfssl_bin');
4✔
457
                if (!$binary) {
4✔
458
                        return [
×
459
                                (new ConfigureCheckHelper())
×
460
                                        ->setErrorMessage('CFSSL not installed.')
×
461
                                        ->setResource('cfssl')
×
462
                                        ->setTip('Run occ libresign:install --cfssl'),
×
463
                        ];
×
464
                }
465

466
                if (!file_exists($binary)) {
4✔
467
                        return [
×
468
                                (new ConfigureCheckHelper())
×
469
                                        ->setErrorMessage('CFSSL not found.')
×
470
                                        ->setResource('cfssl')
×
471
                                        ->setTip('Run occ libresign:install --cfssl'),
×
472
                        ];
×
473
                }
474
                $process = $this->createProcess([$binary, 'version']);
4✔
475
                $process->run();
4✔
476
                $version = $process->getOutput();
4✔
477
                if (!$process->isSuccessful() || empty($version)) {
4✔
478
                        return [
1✔
479
                                (new ConfigureCheckHelper())
1✔
480
                                        ->setErrorMessage(sprintf(
1✔
481
                                                'Failed to run the command "%s" with user %s',
1✔
482
                                                "$binary version",
1✔
483
                                                get_current_user()
1✔
484
                                        ))
1✔
485
                                        ->setResource('cfssl')
1✔
486
                                        ->setTip('Run occ libresign:install --cfssl')
1✔
487
                        ];
1✔
488
                }
489
                preg_match_all('/: (?<version>.*)/', $version, $matches);
3✔
490
                if (!$matches || !isset($matches['version']) || count($matches['version']) !== 2) {
3✔
491
                        return [
1✔
492
                                (new ConfigureCheckHelper())
1✔
493
                                        ->setErrorMessage(sprintf(
1✔
494
                                                'Failed to identify cfssl version with command %s',
1✔
495
                                                "$binary version"
1✔
496
                                        ))
1✔
497
                                        ->setResource('cfssl')
1✔
498
                                        ->setTip('Run occ libresign:install --cfssl')
1✔
499
                        ];
1✔
500
                }
501
                if (!str_contains($matches['version'][0], InstallService::CFSSL_VERSION)) {
2✔
502
                        return [
1✔
503
                                (new ConfigureCheckHelper())
1✔
504
                                        ->setErrorMessage(sprintf(
1✔
505
                                                'Invalid version. Expected: %s, actual: %s',
1✔
506
                                                InstallService::CFSSL_VERSION,
1✔
507
                                                $matches['version'][0]
1✔
508
                                        ))
1✔
509
                                        ->setResource('cfssl')
1✔
510
                                        ->setTip('Run occ libresign:install --cfssl')
1✔
511
                        ];
1✔
512
                }
513
                $return = [];
1✔
514
                $return[] = (new ConfigureCheckHelper())
1✔
515
                        ->setSuccessMessage('CFSSL binary path: ' . $binary)
1✔
516
                        ->setResource('cfssl');
1✔
517
                $return[] = (new ConfigureCheckHelper())
1✔
518
                        ->setSuccessMessage('CFSSL version: ' . $matches['version'][0])
1✔
519
                        ->setResource('cfssl');
1✔
520
                $return[] = (new ConfigureCheckHelper())
1✔
521
                        ->setSuccessMessage('Runtime: ' . $matches['version'][1])
1✔
522
                        ->setResource('cfssl');
1✔
523
                return $return;
1✔
524
        }
525

526
        /**
527
         * @param string[] $command
528
         */
529
        protected function createProcess(array $command): Process {
NEW
530
                return new Process($command);
×
531
        }
532

533
        /**
534
         * Get Authority Key Identifier from certificate (needed for CFSSL revocation)
535
         *
536
         * @param string $certificatePem PEM encoded certificate
537
         * @return string Authority Key Identifier in lowercase without colons
538
         */
539
        public function getAuthorityKeyId(string $certificatePem): string {
540
                $cert = openssl_x509_read($certificatePem);
×
541
                if (!$cert) {
×
542
                        throw new \RuntimeException('Invalid certificate format');
×
543
                }
544

545
                $parsed = openssl_x509_parse($cert);
×
546
                if (!$parsed || !isset($parsed['extensions']['authorityKeyIdentifier'])) {
×
547
                        throw new \RuntimeException('Certificate does not contain Authority Key Identifier');
×
548
                }
549

550
                $authKeyId = $parsed['extensions']['authorityKeyIdentifier'];
×
551

552
                if (preg_match('/keyid:([A-Fa-f0-9:]+)/', $authKeyId, $matches)) {
×
553
                        return strtolower(str_replace(':', '', $matches[1]));
×
554
                }
555

556
                throw new \RuntimeException('Could not parse Authority Key Identifier');
×
557
        }
558

559
        /**
560
         * Revoke a certificate using CFSSL API
561
         *
562
         * @param string $serialNumber Certificate serial number in decimal format
563
         * @param string $authorityKeyId Authority key identifier (lowercase, no colons)
564
         * @param string $reason CRLReason description string (e.g., 'superseded', 'keyCompromise')
565
         */
566
        public function revokeCertificate(string $serialNumber, string $authorityKeyId, string $reason): bool {
567
                try {
568
                        $json = [
×
569
                                'json' => [
×
570
                                        'serial' => $serialNumber,
×
571
                                        'authority_key_id' => $authorityKeyId,
×
572
                                        'reason' => $reason,
×
573
                                ],
×
574
                        ];
×
575

576
                        $response = $this->getClient()->request('POST', 'revoke', $json);
×
577

578
                        $responseData = json_decode((string)$response->getBody(), true);
×
579

580
                        if (!isset($responseData['success'])) {
×
581
                                $errorMessage = isset($responseData['errors'])
×
582
                                        ? implode(', ', array_column($responseData['errors'], 'message'))
×
583
                                        : 'Unknown CFSSL error';
×
584
                                throw new \RuntimeException('CFSSL revocation failed: ' . $errorMessage);
×
585
                        }
586

587
                        return $responseData['success'];
×
588

589
                } catch (RequestException|ConnectException $e) {
×
590
                        throw new \RuntimeException('Failed to communicate with CFSSL server: ' . $e->getMessage());
×
591
                } catch (\Throwable $e) {
×
592
                        throw new \RuntimeException('CFSSL certificate revocation error: ' . $e->getMessage());
×
593
                }
594
        }
595

596
        private function persistSerialNumberToCrl(array $parsed): void {
597
                if (!isset($parsed['serialNumberHex']) || !isset($parsed['valid_to'])) {
×
598
                        return;
×
599
                }
600

601
                $serialNumber = $parsed['serialNumberHex'];
×
602

603
                $owner = $this->getCommonName() ?? 'Unknown';
×
604

605
                $expiresAt = null;
×
606
                if (isset($parsed['validTo_time_t'])) {
×
607
                        $expiresAt = new \DateTime('@' . $parsed['validTo_time_t']);
×
608
                }
609

610
                $issuer = $parsed['issuer'] ?? [];
×
611
                $subject = $parsed['subject'] ?? [];
×
612

613
                $this->crlMapper->createCertificate(
×
614
                        $serialNumber,
×
615
                        $owner,
×
616
                        'cfssl',
×
617
                        $this->caIdentifierService->getInstanceId(),
×
618
                        $this->caIdentifierService->getCaIdParsed()['generation'],
×
619
                        new \DateTime(),
×
620
                        $expiresAt,
×
621
                        $issuer,
×
622
                        $subject,
×
623
                        CertificateType::LEAF->value,
×
624
                );
×
625
        }
626

627
        private function persistRootCertificateFromData(string $certPem): void {
628
                $x509Resource = openssl_x509_read($certPem);
×
629
                if (!$x509Resource) {
×
630
                        throw new \RuntimeException('Failed to parse root certificate');
×
631
                }
632

633
                $parsed = openssl_x509_parse($x509Resource);
×
634
                if (!$parsed) {
×
635
                        throw new \RuntimeException('Failed to extract root certificate information');
×
636
                }
637

638
                $serialNumber = $parsed['serialNumberHex'] ?? '';
×
639
                if (empty($serialNumber)) {
×
640
                        throw new \RuntimeException('Root certificate has no serial number');
×
641
                }
642

643
                $owner = $this->getCommonName() ?? 'Root CA';
×
644

645
                $expiresAt = null;
×
646
                if (isset($parsed['validTo_time_t'])) {
×
647
                        $expiresAt = new \DateTime('@' . $parsed['validTo_time_t']);
×
648
                }
649

650
                /** @var array<string, mixed> $issuer */
651
                $issuer = $parsed['issuer'] ?? [];
×
652
                /** @var array<string, mixed> $subject */
653
                $subject = $parsed['subject'] ?? [];
×
654

655
                $this->crlMapper->createCertificate(
×
656
                        $serialNumber,
×
657
                        $owner,
×
658
                        'cfssl',
×
659
                        $this->caIdentifierService->getInstanceId(),
×
660
                        $this->caIdentifierService->getCaIdParsed()['generation'],
×
661
                        new \DateTime(),
×
662
                        $expiresAt,
×
663
                        $issuer,
×
664
                        $subject,
×
665
                        CertificateType::ROOT->value,
×
666
                );
×
667
        }
668
}
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