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

mlocati / ocsp / 15621242067

12 Jun 2025 09:32PM UTC coverage: 69.062% (+1.9%) from 67.155%
15621242067

Pull #16

github

web-flow
Merge b33dc80ac into 46dc41c62
Pull Request #16: Fix implicitly nullable parameter declarations

471 of 682 relevant lines covered (69.06%)

1.63 hits per line

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

74.36
/src/Ocsp.php
1
<?php
2

3
namespace Ocsp;
4

5
use Ocsp\Asn1\Der\Decoder as DerDecoder;
6
use Ocsp\Asn1\Der\Encoder as DerEncoder;
7
use Ocsp\Asn1\Element;
8
use Ocsp\Asn1\Element\AbstractList;
9
use Ocsp\Asn1\Element\GeneralizedTime;
10
use Ocsp\Asn1\Element\Integer;
11
use Ocsp\Asn1\Element\ObjectIdentifier;
12
use Ocsp\Asn1\Element\OctetString;
13
use Ocsp\Asn1\Element\RawPrimitive;
14
use Ocsp\Asn1\Element\Sequence;
15
use Ocsp\Asn1\Tag;
16
use Ocsp\Asn1\TaggableElement;
17
use Ocsp\Asn1\UniversalTagID;
18
use Ocsp\Exception\Asn1DecodingException;
19
use Ocsp\Exception\ResponseException;
20

21
class Ocsp
22
{
23
    /**
24
     * The media type (Content-Type header) to be used when sending the request to the OCSP Responder URL.
25
     *
26
     * @var string
27
     */
28
    const OCSP_REQUEST_MEDIATYPE = 'application/ocsp-request';
29

30
    /**
31
     * The media type (Content-Type header) that should be included in responses from the OCSP Responder URL.
32
     *
33
     * @var string
34
     */
35
    const OCSP_RESPONSE_MEDIATYPE = 'application/ocsp-response';
36

37
    /**
38
     * The decoder to be used to decode DER-encoded data.
39
     *
40
     * @var \Ocsp\Asn1\Der\Decoder
41
     */
42
    private $derDecoder;
43

44
    /**
45
     * The encoder to be used to encode data to DER.
46
     *
47
     * @var \Ocsp\Asn1\Der\Encoder
48
     */
49
    private $derEncoder;
50

51
    /**
52
     * Initialize the instance.
53
     */
54
    public function __construct()
55
    {
56
        $this->derDecoder = new DerDecoder();
2✔
57
        $this->derEncoder = new DerEncoder();
2✔
58
    }
59

60
    /**
61
     * Build the raw body to be sent to the OCSP Responder URL with one request.
62
     *
63
     * @param \Ocsp\Request $request request to be included in the body
64
     *
65
     * @return string
66
     *
67
     * @see https://tools.ietf.org/html/rfc6960#section-4.1.1 for OCSPRequest
68
     */
69
    public function buildOcspRequestBodySingle(Request $request)
70
    {
71
        return $this->buildOcspRequestBody(RequestList::create([$request]));
2✔
72
    }
73

74
    /**
75
     * Build the raw body to be sent to the OCSP Responder URL with a variable number of requests.
76
     *
77
     * @param \Ocsp\RequestList $requests the list of requests to be included in the body
78
     *
79
     * @return string
80
     *
81
     * @see https://tools.ietf.org/html/rfc6960#section-4.1.1 for OCSPRequest
82
     */
83
    public function buildOcspRequestBody(RequestList $requests)
84
    {
85
        $hashAlgorithm = Sequence::create([
2✔
86
            // OBJECT IDENTIFIER [algorithm]
87
            ObjectIdentifier::create('1.3.14.3.2.26'), // SHA1
2✔
88
        ]);
2✔
89
        $requestList = new Sequence();
2✔
90
        foreach ($requests->getRequests() as $request) {
2✔
91
            $requestList->addElement(
2✔
92
                // Request
93
                Sequence::create([
2✔
94
                    // CertID [reqCert]
95
                    Sequence::create([
2✔
96
                        // AlgorithmIdentifier [hashAlgorithm]
97
                        $hashAlgorithm,
2✔
98
                        // OCTET STRING [issuerNameHash]
99
                        OctetString::create(sha1($request->getIssuerNameDer(), true)),
2✔
100
                        // OCTET STRING [issuerKeyHash]
101
                        OctetString::create(sha1($request->getIssuerPublicKeyBytes(), true)),
2✔
102
                        // CertificateSerialNumber [serialNumber]
103
                        Integer::create($request->getCertificateSerialNumber()),
2✔
104
                    ]),
2✔
105
                ])
2✔
106
            );
2✔
107
        }
108

109
        return $this->derEncoder->encodeElement(
2✔
110
            // OCSPRequest
111
            Sequence::create([
2✔
112
                // TBSRequest [tbsRequest]
113
                Sequence::create([
2✔
114
                    $requestList,
2✔
115
                ]),
2✔
116
            ])
2✔
117
        );
2✔
118
    }
119

120
    /**
121
     * Parse the response received from the OCSP Responder when you expect just one certificate revocation status.
122
     *
123
     * @param string $rawResponseBody the raw response from the responder
124
     *
125
     * @throws \Ocsp\Exception\Asn1DecodingException if $rawBody is not a valid response from the OCSP responder
126
     * @throws \Ocsp\Exception\ResponseException:: if the request was not successfull
127
     *
128
     * @return \Ocsp\Response
129
     *
130
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
131
     */
132
    public function decodeOcspResponseSingle($rawResponseBody)
133
    {
134
        $responses = $this->decodeOcspResponse($rawResponseBody)->getResponses();
2✔
135
        if (count($responses) !== 1) {
2✔
136
            throw ResponseException\MultipleResponsesException::create();
×
137
        }
138

139
        return $responses[0];
2✔
140
    }
141

142
    /**
143
     * Parse the response received from the OCSP Responder when you expect a variable number of certificate revocation statuses.
144
     *
145
     * @param string $rawResponseBody the raw response from the responder
146
     *
147
     * @throws \Ocsp\Exception\Asn1DecodingException if $rawBody is not a valid response from the OCSP responder
148
     * @throws \Ocsp\Exception\ResponseException:: if the request was not successfull
149
     *
150
     * @return \Ocsp\ResponseList
151
     *
152
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
153
     */
154
    public function decodeOcspResponse($rawResponseBody)
155
    {
156
        $ocspResponse = $this->derDecoder->decodeElement($rawResponseBody);
2✔
157
        if (!$ocspResponse instanceof Sequence) {
2✔
158
            throw Asn1DecodingException::create('Invalid response type');
×
159
        }
160
        $this->checkResponseStatus($ocspResponse);
2✔
161
        $responseBytes = $ocspResponse->getFirstChildOfType(0, Element::CLASS_CONTEXTSPECIFIC, Tag::ENVIRONMENT_EXPLICIT);
2✔
162
        if (!$responseBytes instanceof Sequence) {
2✔
163
            throw ResponseException\MissingResponseBytesException::create();
×
164
        }
165

166
        return $this->decodeResponseBytes($responseBytes);
2✔
167
    }
168

169
    /**
170
     * Check the OCSP response status.
171
     *
172
     * @param \Ocsp\Asn1\Element\Sequence $ocspResponse
173
     *
174
     * @throws \Ocsp\Exception\ResponseException:: if the request was not successfull
175
     * @throws \Ocsp\Exception\Asn1DecodingException if the response contains invalid data
176
     *
177
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
178
     */
179
    protected function checkResponseStatus(Sequence $ocspResponse)
180
    {
181
        $responseStatus = $ocspResponse->getFirstChildOfType(UniversalTagID::ENUMERATED);
2✔
182
        if ($responseStatus === null) {
2✔
183
            throw Asn1DecodingException::create('Invalid response type');
×
184
        }
185
        switch ($responseStatus->getRawEncodedValue()) {
2✔
186
            case "\x00": // successful (Response has valid confirmations)
2✔
187
                break;
2✔
188
            case "\x01": // malformedRequest (Illegal confirmation request)
×
189
                throw ResponseException\MalformedRequestException::create();
×
190
            case "\x02": // internalError (Internal error in issuer)
×
191
                throw ResponseException\InternalErrorException::create();
×
192
            case "\x03": // tryLater (Try again later)
×
193
                throw ResponseException\TryLaterException::create();
×
194
            case "\x05": // sigRequired (Must sign the request)
×
195
                throw ResponseException\SigRequiredException::create();
×
196
            case "\x06": // unauthorized (Request unauthorized)
×
197
                throw ResponseException\UnauthorizedException::create();
×
198
            default:
199
                throw Asn1DecodingException::create('Invalid response data');
×
200
        }
201
    }
202

203
    /**
204
     * Parse "responseBytes" element of a response received from the OCSP Responder.
205
     *
206
     * @param \Ocsp\Asn1\Element\Sequence $responseBytes
207
     *
208
     * @throws \Ocsp\Exception\Asn1DecodingException
209
     * @throws \Ocsp\Exception\ResponseException
210
     *
211
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
212
     */
213
    protected function decodeResponseBytes(Sequence $responseBytes)
214
    {
215
        $responseType = $responseBytes->getFirstChildOfType(UniversalTagID::OBJECT_IDENTIFIER);
2✔
216
        $response = $responseBytes->getFirstChildOfType(UniversalTagID::OCTET_STRING);
2✔
217
        if ($responseType !== null && $response !== null) {
2✔
218
            switch ($responseType->getIdentifier()) {
2✔
219
                case '1.3.6.1.5.5.7.48.1.1':
2✔
220
                    return $this->decodeBasicResponse($response->getValue());
2✔
221
            }
222
        }
223

224
        throw ResponseException\MissingResponseBytesException::create();
×
225
    }
226

227
    /**
228
     * Parse the "responseBytes" element of a response received from the OCSP Responder.
229
     *
230
     * @param string $responseBytes
231
     *
232
     * @throws \Ocsp\Exception\Asn1DecodingException
233
     * @throws \Ocsp\Exception\ResponseException
234
     *
235
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
236
     *
237
     * @return \Ocsp\ResponseList
238
     */
239
    protected function decodeBasicResponse($responseBytes)
240
    {
241
        $basicOCSPResponse = $this->derDecoder->decodeElement($responseBytes);
2✔
242
        if (!$basicOCSPResponse instanceof Sequence) {
2✔
243
            throw Asn1DecodingException::create();
×
244
        }
245
        $tbsResponseData = $basicOCSPResponse->getFirstChildOfType(UniversalTagID::SEQUENCE);
2✔
246
        if (!$tbsResponseData instanceof Sequence) {
2✔
247
            throw Asn1DecodingException::create();
×
248
        }
249
        $responses = $tbsResponseData->getFirstChildOfType(UniversalTagID::SEQUENCE);
2✔
250
        if (!$responses instanceof Sequence) {
2✔
251
            throw Asn1DecodingException::create();
×
252
        }
253
        $responseList = ResponseList::create();
2✔
254
        foreach ($responses->getElements() as $singleResponse) {
2✔
255
            if ($singleResponse instanceof Sequence && $singleResponse->getTag() === null) {
2✔
256
                $responseList->addResponse($this->decodeBasicSingleResponse($singleResponse));
2✔
257
            }
258
        }
259
        if ($responseList->getResponses() === []) {
2✔
260
            throw ResponseException\MissingResponseBytesException::create();
×
261
        }
262

263
        return $responseList;
2✔
264
    }
265

266
    /**
267
     * Parse a "SingleResponse" element of a BasicOCSPResponse.
268
     *
269
     * @param \Ocsp\Asn1\Element\Sequence $singleResponse
270
     *
271
     * @throws \Ocsp\Exception\Asn1DecodingException
272
     * @throws \Ocsp\Exception\ResponseException
273
     *
274
     * @return \Ocsp\Response
275
     *
276
     * @see https://tools.ietf.org/html/rfc6960#section-4.2.1
277
     */
278
    protected function decodeBasicSingleResponse(Sequence $singleResponse)
279
    {
280
        $elements = $singleResponse->getElements();
2✔
281
        $certID = isset($elements[0]) ? $elements[0] : null;
2✔
282
        if (!$certID instanceof Sequence) {
2✔
283
            throw Asn1DecodingException::create();
×
284
        }
285

286
        $certificateSerialNumber = (string) $certID->getFirstChildOfType(UniversalTagID::INTEGER, Element::CLASS_UNIVERSAL)->getValue();
2✔
287
        $thisUpdate = $singleResponse->getFirstChildOfType(UniversalTagID::GENERALIZEDTIME, Element::CLASS_UNIVERSAL)->getValue();
2✔
288
        $nextUpdate = $singleResponse->getFirstChildOfType(0, Element::CLASS_CONTEXTSPECIFIC, Tag::ENVIRONMENT_EXPLICIT);
2✔
289
        if ($nextUpdate !== null) {
2✔
290
            $nextUpdate = $nextUpdate->getValue();
2✔
291
        }
292

293
        $certStatus = isset($elements[1]) ? $elements[1] : null;
2✔
294
        if ($certStatus === null) {
2✔
295
            throw Asn1DecodingException::create();
×
296
        }
297
        $certStatusTag = $certStatus instanceof TaggableElement ? $certStatus->getTag() : null;
2✔
298
        if ($certStatusTag === null) {
2✔
299
            if ($certStatus->getClass() !== Element::CLASS_CONTEXTSPECIFIC) {
2✔
300
                throw Asn1DecodingException::create();
×
301
            }
302
            $tagID = $certStatus->getTypeID();
2✔
303
        } else {
304
            if ($certStatusTag->getClass() !== Element::CLASS_CONTEXTSPECIFIC) {
×
305
                throw Asn1DecodingException::create();
×
306
            }
307
            $tagID = $certStatusTag->getTagID();
×
308
        }
309
        switch ($tagID) {
310
            case 0:
2✔
311
                return Response::good($thisUpdate, $certificateSerialNumber, $nextUpdate);
1✔
312
            case 1:
1✔
313
                $revokedOn = null;
1✔
314
                $revocationReason = Response::REVOCATIONREASON_UNSPECIFIED;
1✔
315
                if ($certStatus instanceof GeneralizedTime) {
1✔
316
                    $revokedOn = $certStatus->getValue();
×
317
                } elseif ($certStatus instanceof AbstractList) {
1✔
318
                    $certStatusChildren = $certStatus->getElements();
1✔
319
                    if (isset($certStatusChildren[0]) && $certStatusChildren[0] instanceof GeneralizedTime) {
1✔
320
                        $revokedOn = $certStatusChildren[0]->getValue();
1✔
321
                        if (isset($certStatusChildren[1]) && $certStatusChildren[1] instanceof RawPrimitive) {
1✔
322
                            $bitString = $certStatusChildren[1]->getRawEncodedValue();
1✔
323
                            if (strlen($bitString) === 1) {
1✔
324
                                $revocationReason = ord($bitString[0]);
1✔
325
                            }
326
                        }
327
                    }
328
                }
329
                if ($revokedOn === null) {
1✔
330
                    throw Asn1DecodingException::create('Failed to find the revocation date/time');
×
331
                }
332

333
                return Response::revoked($thisUpdate, $certificateSerialNumber, $revokedOn, $revocationReason, $nextUpdate);
1✔
334
            case 2:
×
335
            default:
336
                return Response::unknown($thisUpdate, $certificateSerialNumber, $nextUpdate);
×
337
        }
338
    }
339
}
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

© 2025 Coveralls, Inc