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

LibreSign / libresign / 19722922474

27 Nov 2025 02:06AM UTC coverage: 39.9%. First build
19722922474

Pull #5828

github

web-flow
Merge 6d6bc1a4f into ec6572ee5
Pull Request #5828: feat: crl manager

0 of 142 new or added lines in 3 files covered. (0.0%)

4772 of 11960 relevant lines covered (39.9%)

3.47 hits per line

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

52.73
/lib/Service/CrlService.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9

10
namespace OCA\Libresign\Service;
11

12
use DateTime;
13
use OCA\Libresign\Db\CrlMapper;
14
use OCA\Libresign\Enum\CRLReason;
15
use OCA\Libresign\Handler\CertificateEngine\CertificateEngineFactory;
16
use Psr\Log\LoggerInterface;
17

18
/**
19
 * RFC 5280 compliant CRL management
20
 */
21
class CrlService {
22

23
        public function __construct(
24
                private CrlMapper $crlMapper,
25
                private LoggerInterface $logger,
26
                private CertificateEngineFactory $certificateEngineFactory,
27
        ) {
28
        }
12✔
29

30
        private static function isValidReasonCode(int $reasonCode): bool {
31
                return CRLReason::isValid($reasonCode);
2✔
32
        }
33

34

35

36
        public function revokeCertificate(
37
                string $serialNumber,
38
                int $reasonCode = CRLReason::UNSPECIFIED->value,
39
                ?string $reasonText = null,
40
                ?string $revokedBy = null,
41
                ?DateTime $invalidityDate = null,
42
        ): bool {
43
                if (!self::isValidReasonCode($reasonCode)) {
2✔
44
                        throw new \InvalidArgumentException("Invalid CRLReason code: {$reasonCode}");
1✔
45
                }
46

47
                $reason = CRLReason::from($reasonCode);
1✔
48

49
                try {
50
                        $certificate = $this->crlMapper->findBySerialNumber($serialNumber);
1✔
51
                        $instanceId = $certificate->getInstanceId();
1✔
52
                        $generation = $certificate->getGeneration();
1✔
53
                        $engineType = $certificate->getEngine();
1✔
54

55
                        $crlNumber = $this->getNextCrlNumber($instanceId, $generation, $engineType);
1✔
56

57
                        $this->crlMapper->revokeCertificate(
1✔
58
                                $serialNumber,
1✔
59
                                $reason,
1✔
60
                                $reasonText,
1✔
61
                                $revokedBy,
1✔
62
                                $invalidityDate,
1✔
63
                                $crlNumber
1✔
64
                        );
1✔
65

66
                        return true;
1✔
67
                } catch (\Exception $e) {
×
68
                        return false;
×
69
                }
70
        }
71

72
        public function getCertificateStatus(string $serialNumber, ?DateTime $checkDate = null): array {
73
                try {
74
                        $certificate = $this->crlMapper->findBySerialNumber($serialNumber);
3✔
75

76
                        if ($certificate->isRevoked()) {
×
77
                                return [
×
78
                                        'status' => 'revoked',
×
79
                                        'reason_code' => $certificate->getReasonCode(),
×
80
                                        'revoked_at' => $certificate->getRevokedAt()?->format('Y-m-d\TH:i:s\Z'),
×
81
                                ];
×
82
                        }
83

84
                        if ($certificate->isExpired()) {
×
85
                                return [
×
86
                                        'status' => 'expired',
×
87
                                        'valid_to' => $certificate->getValidTo()?->format('Y-m-d\TH:i:s\Z'),
×
88
                                ];
×
89
                        }
90

91
                        return ['status' => 'valid'];
×
92

93
                } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
3✔
94
                        return ['status' => 'unknown'];
3✔
95
                }
96
        }
97

98
        public function getCertificateStatusResponse(string $serialNumber): array {
99
                $statusInfo = $this->getCertificateStatus($serialNumber);
3✔
100

101
                $response = [
3✔
102
                        'serial_number' => $serialNumber,
3✔
103
                        'status' => $statusInfo['status'],
3✔
104
                        'checked_at' => (new \DateTime())->format('Y-m-d\TH:i:s\Z'),
3✔
105
                ];
3✔
106

107
                if ($statusInfo['status'] === 'revoked') {
3✔
108
                        if (isset($statusInfo['reason_code'])) {
×
109
                                $response['reason_code'] = $statusInfo['reason_code'];
×
110
                        }
111
                        if (isset($statusInfo['revoked_at'])) {
×
112
                                $response['revoked_at'] = $statusInfo['revoked_at'];
×
113
                        }
114
                }
115

116
                if ($statusInfo['status'] === 'expired') {
3✔
117
                        if (isset($statusInfo['valid_to'])) {
×
118
                                $response['valid_to'] = $statusInfo['valid_to'];
×
119
                        }
120
                }
121

122
                return $response;
3✔
123
        }
124

125
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
126
                return $this->crlMapper->isInvalidAt($serialNumber, $checkDate);
×
127
        }
128

129
        public function getRevokedCertificates(string $instanceId = '', int $generation = 0, string $engine = ''): array {
130
                $certificates = $this->crlMapper->getRevokedCertificates($instanceId, $generation, $engine);
1✔
131

132
                $result = [];
1✔
133
                foreach ($certificates as $certificate) {
1✔
134
                        $result[] = [
1✔
135
                                'serial_number' => $certificate->getSerialNumber(),
1✔
136
                                'owner' => $certificate->getOwner(),
1✔
137
                                'reason_code' => $certificate->getReasonCode(),
1✔
138
                                'description' => $certificate->getReasonCode() ? CRLReason::from($certificate->getReasonCode())->getDescription() : null,
1✔
139
                                'revoked_by' => $certificate->getRevokedBy(),
1✔
140
                                'revoked_at' => $certificate->getRevokedAt()?->format('Y-m-d H:i:s'),
1✔
141
                                'invalidity_date' => $certificate->getInvalidityDate()?->format('Y-m-d H:i:s'),
1✔
142
                                'crl_number' => $certificate->getCrlNumber(),
1✔
143
                        ];
1✔
144
                }
145

146
                return $result;
1✔
147
        }
148

149
        private function getNextCrlNumber(string $instanceId, int $generation, string $engineType): int {
150
                $lastCrlNumber = $this->crlMapper->getLastCrlNumber($instanceId, $generation, $engineType);
8✔
151

152
                return $lastCrlNumber + 1;
8✔
153
        }
154

155
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
156
                return $this->crlMapper->cleanupExpiredCertificates($before);
×
157
        }
158

159
        public function getStatistics(): array {
160
                return $this->crlMapper->getStatistics();
1✔
161
        }
162

163
        public function getRevocationStatistics(): array {
164
                return $this->crlMapper->getRevocationStatistics();
×
165
        }
166

167
        public function generateCrlDer(string $instanceId, int $generation, string $engineType): string {
168
                try {
169
                        $revokedCertificates = $this->crlMapper->getRevokedCertificates($instanceId, $generation, $engineType);
7✔
170

171
                        $engine = $this->certificateEngineFactory->getEngine();
7✔
172

173
                        if (!method_exists($engine, 'generateCrlDer')) {
7✔
174
                                throw new \RuntimeException('Current certificate engine does not support CRL generation');
×
175
                        }
176

177
                        $crlNumber = $this->getNextCrlNumber($instanceId, $generation, $engineType);
7✔
178

179
                        return $engine->generateCrlDer($revokedCertificates, $instanceId, $generation, $crlNumber);
7✔
180
                } catch (\Throwable $e) {
5✔
181
                        $this->logger->error('Failed to generate CRL', [
5✔
182
                                'exception' => $e,
5✔
183
                        ]);
5✔
184
                        throw $e;
5✔
185
                }
186
        }
187

188
        /**
189
         * List CRL entries with pagination and filters
190
         *
191
         * @param int|null $page Page number (1-based), defaults to 1
192
         * @param int|null $length Number of items per page, defaults to 100
193
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, serial_number, revoked_by, generation)
194
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
195
         * @return array{data: array<array<string, mixed>>, total: int, page: int, length: int}
196
         */
197
        public function listCrlEntries(
198
                ?int $page = null,
199
                ?int $length = null,
200
                array $filter = [],
201
                array $sort = [],
202
        ): array {
NEW
203
                $page ??= 1;
×
NEW
204
                $length ??= 100;
×
205

NEW
206
                $result = $this->crlMapper->listWithPagination($page, $length, $filter, $sort);
×
207

NEW
208
                $formattedData = array_map(function ($entity) {
×
NEW
209
                        return [
×
NEW
210
                                'id' => $entity->getId(),
×
NEW
211
                                'serial_number' => $entity->getSerialNumber(),
×
NEW
212
                                'owner' => $entity->getOwner(),
×
NEW
213
                                'status' => $entity->getStatus(),
×
NEW
214
                                'engine' => $entity->getEngine(),
×
NEW
215
                                'instance_id' => $entity->getInstanceId(),
×
NEW
216
                                'generation' => $entity->getGeneration(),
×
NEW
217
                                'issued_at' => $entity->getIssuedAt()?->format('Y-m-d H:i:s'),
×
NEW
218
                                'valid_to' => $entity->getValidTo()?->format('Y-m-d H:i:s'),
×
NEW
219
                                'revoked_at' => $entity->getRevokedAt()?->format('Y-m-d H:i:s'),
×
NEW
220
                                'reason_code' => $entity->getReasonCode(),
×
NEW
221
                                'comment' => $entity->getComment(),
×
NEW
222
                                'revoked_by' => $entity->getRevokedBy(),
×
NEW
223
                                'invalidity_date' => $entity->getInvalidityDate()?->format('Y-m-d H:i:s'),
×
NEW
224
                                'crl_number' => $entity->getCrlNumber(),
×
NEW
225
                        ];
×
NEW
226
                }, $result['data']);
×
227

NEW
228
                return [
×
NEW
229
                        'data' => $formattedData,
×
NEW
230
                        'total' => $result['total'],
×
NEW
231
                        'page' => $page,
×
NEW
232
                        'length' => $length,
×
NEW
233
                ];
×
234
        }
235

236
}
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