Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.09% covered (warning)
68.09%
32 / 47
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CrlClient
68.09% covered (warning)
68.09%
32 / 47
50.00% covered (danger)
50.00%
2 / 4
24.32
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
 getCrl
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 fetchCrl
56.25% covered (warning)
56.25%
18 / 32
0.00% covered (danger)
0.00%
0 / 1
13.36
 assertHttpUrl
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Core\Interactive\Signature;
6
7/**
8 * CRL (Certificate Revocation List) fetcher.
9 *
10 * Extracts CRL Distribution Point URLs from a certificate's CDP
11 * extension and fetches the CRL via HTTP GET. Returns raw DER-encoded
12 * CRL bytes suitable for embedding in a {@see \Phpdftk\Pdf\Core\Document\DSS}.
13 */
14final class CrlClient
15{
16    private int $timeout;
17
18    /**
19     * @param int $timeout HTTP request timeout in seconds
20     */
21    public function __construct(int $timeout = 30)
22    {
23        $this->timeout = $timeout;
24    }
25
26    /**
27     * Fetch the CRL for a certificate from its CRL Distribution Points.
28     *
29     * Tries each CDP URL in order until one succeeds.
30     *
31     * @param string $derCert DER-encoded certificate
32     * @return string Raw DER-encoded CRL
33     * @throws \RuntimeException if no CDP is present or all URLs fail
34     */
35    public function getCrl(string $derCert): string
36    {
37        $urls = CertificateUtils::getCrlDistributionPointUrls($derCert);
38        if (empty($urls)) {
39            throw new \RuntimeException(
40                'Certificate does not contain CRL Distribution Point URLs (no CDP extension)',
41            );
42        }
43
44        $lastError = '';
45        foreach ($urls as $url) {
46            try {
47                return $this->fetchCrl($url);
48            } catch (\RuntimeException $e) {
49                $lastError = $e->getMessage();
50            }
51        }
52
53        throw new \RuntimeException("Failed to fetch CRL from any distribution point: $lastError");
54    }
55
56    /**
57     * Fetch a CRL from the given URL via HTTP GET.
58     *
59     * Automatically detects PEM vs DER format and converts PEM to DER.
60     *
61     * @param string $url HTTP/HTTPS URL to the CRL
62     * @return string Raw DER-encoded CRL
63     * @throws \RuntimeException on network error or invalid response
64     */
65    public function fetchCrl(string $url): string
66    {
67        self::assertHttpUrl($url);
68
69        $ch = curl_init($url);
70        if ($ch === false) {
71            throw new \RuntimeException('Failed to initialize cURL for CRL request');
72        }
73
74        curl_setopt_array($ch, [
75            CURLOPT_HTTPGET => true,
76            CURLOPT_RETURNTRANSFER => true,
77            CURLOPT_TIMEOUT => $this->timeout,
78            CURLOPT_FOLLOWLOCATION => true,
79            CURLOPT_MAXREDIRS => 3,
80            CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
81            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
82        ]);
83
84        $response = curl_exec($ch);
85        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
86        $error = curl_error($ch);
87        curl_close($ch);
88
89        if ($response === false) {
90            throw new \RuntimeException("CRL fetch failed: $error");
91        }
92
93        if ($httpCode !== 200) {
94            throw new \RuntimeException("CRL server returned HTTP $httpCode");
95        }
96
97        $data = (string) $response;
98        if ($data === '') {
99            throw new \RuntimeException('CRL response is empty');
100        }
101
102        // Auto-detect PEM format and convert to DER
103        if (str_contains($data, '-----BEGIN X509 CRL-----')) {
104            $pem = preg_replace('/-----[A-Z0-9 ]+-----/', '', $data) ?? '';
105            $pem = preg_replace('/\s+/', '', $pem) ?? '';
106            $der = base64_decode($pem, true);
107            if ($der === false || $der === '') {
108                throw new \RuntimeException('Failed to decode PEM CRL to DER');
109            }
110            return $der;
111        }
112
113        return $data;
114    }
115
116    private static function assertHttpUrl(string $url): void
117    {
118        $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
119        if ($scheme !== 'http' && $scheme !== 'https') {
120            throw new \InvalidArgumentException("Only HTTP and HTTPS CRL URLs are allowed: $url");
121        }
122    }
123}