Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.25% covered (warning)
56.25%
18 / 32
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CrlController
56.25% covered (warning)
56.25%
18 / 32
50.00% covered (danger)
50.00%
2 / 4
15.78
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevocationList
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 checkCertificateStatus
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 isValidSerial
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
1<?php
2
3declare(strict_types=1);
4
5/**
6 * SPDX-FileCopyrightText: 2025 LibreCode coop and contributors
7 * SPDX-License-Identifier: AGPL-3.0-or-later
8 */
9
10namespace OCA\Libresign\Controller;
11
12use OCA\Libresign\Service\Crl\CrlService;
13use OCP\AppFramework\Controller;
14use OCP\AppFramework\Http;
15use OCP\AppFramework\Http\Attribute\FrontpageRoute;
16use OCP\AppFramework\Http\Attribute\NoAdminRequired;
17use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
18use OCP\AppFramework\Http\Attribute\PublicPage;
19use OCP\AppFramework\Http\DataDownloadResponse;
20use OCP\AppFramework\Http\DataResponse;
21use OCP\IRequest;
22use Psr\Log\LoggerInterface;
23
24/**
25 * @psalm-import-type LibresignCrlCertificateStatusResponse from \OCA\Libresign\ResponseDefinitions
26 * @psalm-import-type LibresignCrlErrorResponse from \OCA\Libresign\ResponseDefinitions
27 */
28class CrlController extends Controller {
29    public function __construct(
30        string $appName,
31        IRequest $request,
32        private CrlService $crlService,
33        private LoggerInterface $logger,
34    ) {
35        parent::__construct($appName, $request);
36    }
37
38    /**
39     * Get Certificate Revocation List in DER format (RFC 5280 compliant)
40     *
41     * @param string $instanceId Instance identifier
42     * @param int $generation Generation identifier
43     * @param string $engineType Engine type identifier
44     * @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, LibresignCrlErrorResponse, array{}>
45     *
46     * 200: CRL retrieved successfully in DER format
47     * 500: Failed to generate CRL
48     */
49    #[NoAdminRequired]
50    #[NoCSRFRequired]
51    #[PublicPage]
52    #[FrontpageRoute(verb: 'GET', url: '/crl/libresign_{instanceId}_{generation}_{engineType}.crl')]
53    public function getRevocationList(string $instanceId, int $generation, string $engineType): DataDownloadResponse|DataResponse {
54        try {
55            $crlDer = $this->crlService->generateCrlDer($instanceId, $generation, $engineType);
56
57            return new DataDownloadResponse(
58                $crlDer,
59                'libresign_' . $instanceId . '_' . $generation . '_' . $engineType . '.crl',
60                'application/pkix-crl'
61            );
62        } catch (\Throwable $e) {
63            $this->logger->error('Failed to generate CRL', ['exception' => $e]);
64
65            return new DataResponse([
66                'error' => 'CRL generation failed',
67                'message' => $e->getMessage()
68            ], Http::STATUS_INTERNAL_SERVER_ERROR);
69        }
70    }
71
72    /**
73     * Check certificate revocation status
74     *
75     * @param string $serialNumber Certificate serial number to check
76     * @return DataResponse<Http::STATUS_OK, LibresignCrlCertificateStatusResponse, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, LibresignCrlErrorResponse, array{}>
77     *
78     * 200: Certificate status retrieved successfully
79     * 400: Invalid serial number format
80     */
81    #[NoAdminRequired]
82    #[NoCSRFRequired]
83    #[PublicPage]
84    #[FrontpageRoute(verb: 'GET', url: '/crl/check/{serialNumber}')]
85    public function checkCertificateStatus(string $serialNumber): DataResponse {
86        if (!$this->isValidSerial($serialNumber)) {
87            /** @var LibresignCrlErrorResponse $errorResponse */
88            $errorResponse = [
89                'error' => 'Invalid serial number',
90                'message' => 'Serial number must be numeric (decimal or hex format, no 0x prefix)',
91            ];
92
93            return new DataResponse(
94                $errorResponse,
95                Http::STATUS_BAD_REQUEST
96            );
97        }
98
99        /** @var LibresignCrlCertificateStatusResponse $statusResponse */
100        $statusResponse = $this->crlService->getCertificateStatusResponse($serialNumber);
101
102        return new DataResponse($statusResponse);
103    }
104
105    private function isValidSerial(string $serialNumber): bool {
106        $serialNumber = trim($serialNumber);
107
108        if ($serialNumber === '') {
109            return false;
110        }
111
112        if (str_starts_with(strtolower($serialNumber), '0x')) {
113            return false;
114        }
115
116        if (ctype_digit($serialNumber)) {
117            return true;
118        }
119
120        return ctype_xdigit($serialNumber);
121    }
122}