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

LibreSign / libresign / 19839372203

01 Dec 2025 10:16PM UTC coverage: 40.771%. First build
19839372203

Pull #5872

github

web-flow
Merge 7fad3608b into c3ec57e4c
Pull Request #5872: feat: certificate type classification

33 of 70 new or added lines in 7 files covered. (47.14%)

4963 of 12173 relevant lines covered (40.77%)

3.99 hits per line

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

62.77
/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
        }
17✔
29

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

34

35

36
        public function revokeCertificate(
37
                string $serialNumber,
38
                CRLReason $reason = CRLReason::UNSPECIFIED,
39
                ?string $reasonText = null,
40
                ?string $revokedBy = null,
41
                ?DateTime $invalidityDate = null,
42
        ): bool {
43

44
                try {
45
                        $certificate = $this->crlMapper->findBySerialNumber($serialNumber);
1✔
46
                        $instanceId = $certificate->getInstanceId();
1✔
47
                        $generation = $certificate->getGeneration();
1✔
48
                        $engineType = $certificate->getEngine();
1✔
49

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

52
                        $this->crlMapper->revokeCertificate(
1✔
53
                                $serialNumber,
1✔
54
                                $reason,
1✔
55
                                $reasonText,
1✔
56
                                $revokedBy,
1✔
57
                                $invalidityDate,
1✔
58
                                $crlNumber
1✔
59
                        );
1✔
60

61
                        return true;
1✔
62
                } catch (\Exception $e) {
×
63
                        return false;
×
64
                }
65
        }
66

67
        /**
68
         * Revoke all issued certificates owned by a user
69
         *
70
         * @param string $userId User ID whose certificates should be revoked
71
         * @param CRLReason $reason Revocation reason
72
         * @param string|null $reasonText Optional text describing the reason
73
         * @param string|null $revokedBy Who is revoking the certificates
74
         * @return int Number of certificates revoked
75
         */
76
        public function revokeUserCertificates(
77
                string $userId,
78
                CRLReason $reason = CRLReason::UNSPECIFIED,
79
                ?string $reasonText = null,
80
                ?string $revokedBy = null,
81
        ): int {
82
                $certificates = $this->crlMapper->findIssuedByOwner($userId);
6✔
83

84
                return $this->revokeCertificateList(
6✔
85
                        $certificates,
6✔
86
                        $reason,
6✔
87
                        $reasonText,
6✔
88
                        $revokedBy
6✔
89
                );
6✔
90
        }
91

92
        /**
93
         * Revoke a list of certificates
94
         *
95
         * @param array<\OCA\Libresign\Db\Crl> $certificates Array of Crl entities
96
         * @param CRLReason $reason Revocation reason
97
         * @param string|null $reasonText Optional text describing the reason
98
         * @param string|null $revokedBy Who is revoking the certificates
99
         * @return int Number of certificates successfully revoked
100
         */
101
        private function revokeCertificateList(
102
                array $certificates,
103
                CRLReason $reason,
104
                ?string $reasonText = null,
105
                ?string $revokedBy = null,
106
        ): int {
107
                $revokedCount = 0;
6✔
108

109
                foreach ($certificates as $certificate) {
6✔
110
                        try {
111
                                $instanceId = $certificate->getInstanceId();
5✔
112
                                $generation = $certificate->getGeneration();
5✔
113
                                $engineType = $certificate->getEngine();
5✔
114
                                $serialNumber = $certificate->getSerialNumber();
5✔
115

116
                                $crlNumber = $this->getNextCrlNumber($instanceId, $generation, $engineType);
5✔
117

118
                                $this->crlMapper->revokeCertificate(
5✔
119
                                        $serialNumber,
5✔
120
                                        $reason,
5✔
121
                                        $reasonText,
5✔
122
                                        $revokedBy,
5✔
123
                                        null,
5✔
124
                                        $crlNumber
5✔
125
                                );
5✔
126

127
                                $revokedCount++;
5✔
128
                        } catch (\Exception $e) {
2✔
129
                                $this->logger->warning('Failed to revoke certificate {serial}', [
2✔
130
                                        'serial' => $certificate->getSerialNumber(),
2✔
131
                                        'error' => $e->getMessage(),
2✔
132
                                ]);
2✔
133
                        }
134
                }
135

136
                return $revokedCount;
6✔
137
        }
138

139
        public function getCertificateStatus(string $serialNumber, ?DateTime $checkDate = null): array {
140
                try {
141
                        $certificate = $this->crlMapper->findBySerialNumber($serialNumber);
22✔
142

143
                        if ($certificate->isRevoked()) {
13✔
144
                                return [
×
145
                                        'status' => 'revoked',
×
146
                                        'reason_code' => $certificate->getReasonCode(),
×
147
                                        'revoked_at' => $certificate->getRevokedAt()?->format('Y-m-d\TH:i:s\Z'),
×
148
                                ];
×
149
                        }
150

151
                        if ($certificate->isExpired()) {
13✔
152
                                return [
×
153
                                        'status' => 'expired',
×
154
                                        'valid_to' => $certificate->getValidTo()?->format('Y-m-d\TH:i:s\Z'),
×
155
                                ];
×
156
                        }
157

158
                        return ['status' => 'valid'];
13✔
159

160
                } catch (\OCP\AppFramework\Db\DoesNotExistException $e) {
9✔
161
                        return ['status' => 'unknown'];
9✔
162
                }
163
        }
164

165
        public function getCertificateStatusResponse(string $serialNumber): array {
166
                $statusInfo = $this->getCertificateStatus($serialNumber);
3✔
167

168
                $response = [
3✔
169
                        'serial_number' => $serialNumber,
3✔
170
                        'status' => $statusInfo['status'],
3✔
171
                        'checked_at' => (new \DateTime())->format('Y-m-d\TH:i:s\Z'),
3✔
172
                ];
3✔
173

174
                if ($statusInfo['status'] === 'revoked') {
3✔
175
                        if (isset($statusInfo['reason_code'])) {
×
176
                                $response['reason_code'] = $statusInfo['reason_code'];
×
177
                        }
178
                        if (isset($statusInfo['revoked_at'])) {
×
179
                                $response['revoked_at'] = $statusInfo['revoked_at'];
×
180
                        }
181
                }
182

183
                if ($statusInfo['status'] === 'expired') {
3✔
184
                        if (isset($statusInfo['valid_to'])) {
×
185
                                $response['valid_to'] = $statusInfo['valid_to'];
×
186
                        }
187
                }
188

189
                return $response;
3✔
190
        }
191

192
        public function isInvalidAt(string $serialNumber, ?DateTime $checkDate = null): bool {
193
                return $this->crlMapper->isInvalidAt($serialNumber, $checkDate);
×
194
        }
195

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

199
                $result = [];
1✔
200
                foreach ($certificates as $certificate) {
1✔
201
                        $result[] = [
1✔
202
                                'serial_number' => $certificate->getSerialNumber(),
1✔
203
                                'owner' => $certificate->getOwner(),
1✔
204
                                'reason_code' => $certificate->getReasonCode(),
1✔
205
                                'description' => $certificate->getReasonCode() ? CRLReason::from($certificate->getReasonCode())->getDescription() : null,
1✔
206
                                'revoked_by' => $certificate->getRevokedBy(),
1✔
207
                                'revoked_at' => $certificate->getRevokedAt()?->format('Y-m-d H:i:s'),
1✔
208
                                'invalidity_date' => $certificate->getInvalidityDate()?->format('Y-m-d H:i:s'),
1✔
209
                                'crl_number' => $certificate->getCrlNumber(),
1✔
210
                        ];
1✔
211
                }
212

213
                return $result;
1✔
214
        }
215

216
        private function getNextCrlNumber(string $instanceId, int $generation, string $engineType): int {
217
                $lastCrlNumber = $this->crlMapper->getLastCrlNumber($instanceId, $generation, $engineType);
14✔
218

219
                return $lastCrlNumber + 1;
14✔
220
        }
221

222
        public function cleanupExpiredCertificates(?DateTime $before = null): int {
223
                return $this->crlMapper->cleanupExpiredCertificates($before);
×
224
        }
225

226
        public function getStatistics(): array {
227
                return $this->crlMapper->getStatistics();
1✔
228
        }
229

230
        public function getRevocationStatistics(): array {
231
                return $this->crlMapper->getRevocationStatistics();
×
232
        }
233

234
        public function generateCrlDer(string $instanceId, int $generation, string $engineType): string {
235
                try {
236
                        $revokedCertificates = $this->crlMapper->getRevokedCertificates($instanceId, $generation, $engineType);
8✔
237

238
                        $engine = $this->certificateEngineFactory->getEngine();
8✔
239

240
                        if (!method_exists($engine, 'generateCrlDer')) {
8✔
241
                                throw new \RuntimeException('Current certificate engine does not support CRL generation');
×
242
                        }
243

244
                        $crlNumber = $this->getNextCrlNumber($instanceId, $generation, $engineType);
8✔
245

246
                        return $engine->generateCrlDer($revokedCertificates, $instanceId, $generation, $crlNumber);
8✔
247
                } catch (\Throwable $e) {
6✔
248
                        $this->logger->error('Failed to generate CRL', [
6✔
249
                                'exception' => $e,
6✔
250
                        ]);
6✔
251
                        throw $e;
6✔
252
                }
253
        }
254

255
        /**
256
         * List CRL entries with pagination and filters
257
         *
258
         * @param int|null $page Page number (1-based), defaults to 1
259
         * @param int|null $length Number of items per page, defaults to 100
260
         * @param array<string, mixed> $filter Filters to apply (status, engine, instance_id, owner, serial_number, revoked_by, generation)
261
         * @param array<string, string> $sort Sort fields and directions ['field' => 'ASC|DESC']
262
         * @return array{data: array<array<string, mixed>>, total: int, page: int, length: int}
263
         */
264
        public function listCrlEntries(
265
                ?int $page = null,
266
                ?int $length = null,
267
                array $filter = [],
268
                array $sort = [],
269
        ): array {
270
                $page ??= 1;
×
271
                $length ??= 100;
×
272

273
                $result = $this->crlMapper->listWithPagination($page, $length, $filter, $sort);
×
274

275
                $formattedData = array_map(function ($entity) {
×
276
                        return [
×
277
                                'id' => $entity->getId(),
×
278
                                'serial_number' => $entity->getSerialNumber(),
×
279
                                'owner' => $entity->getOwner(),
×
280
                                'status' => $entity->getStatus(),
×
NEW
281
                                'certificate_type' => $entity->getCertificateType(),
×
282
                                'engine' => $entity->getEngine(),
×
283
                                'instance_id' => $entity->getInstanceId(),
×
284
                                'generation' => $entity->getGeneration(),
×
285
                                'issued_at' => $entity->getIssuedAt()?->format('Y-m-d H:i:s'),
×
286
                                'valid_to' => $entity->getValidTo()?->format('Y-m-d H:i:s'),
×
287
                                'revoked_at' => $entity->getRevokedAt()?->format('Y-m-d H:i:s'),
×
288
                                'reason_code' => $entity->getReasonCode(),
×
289
                                'comment' => $entity->getComment(),
×
290
                                'revoked_by' => $entity->getRevokedBy(),
×
291
                                'invalidity_date' => $entity->getInvalidityDate()?->format('Y-m-d H:i:s'),
×
292
                                'crl_number' => $entity->getCrlNumber(),
×
293
                        ];
×
294
                }, $result['data']);
×
295

296
                return [
×
297
                        'data' => $formattedData,
×
298
                        'total' => $result['total'],
×
299
                        'page' => $page,
×
300
                        'length' => $length,
×
301
                ];
×
302
        }
303

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