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 | 2 | public function __construct() |
|||
55 | { |
||||
56 | 2 | $this->derDecoder = new DerDecoder(); |
|||
57 | 2 | $this->derEncoder = new DerEncoder(); |
|||
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 | 2 | public function buildOcspRequestBodySingle(Request $request) |
|||
70 | { |
||||
71 | 2 | return $this->buildOcspRequestBody(RequestList::create([$request])); |
|||
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 | 2 | public function buildOcspRequestBody(RequestList $requests) |
|||
84 | { |
||||
85 | 2 | $hashAlgorithm = Sequence::create([ |
|||
86 | // OBJECT IDENTIFIER [algorithm] |
||||
87 | 2 | ObjectIdentifier::create('1.3.14.3.2.26'), // SHA1 |
|||
88 | 2 | ]); |
|||
89 | 2 | $requestList = new Sequence(); |
|||
90 | 2 | foreach ($requests->getRequests() as $request) { |
|||
91 | 2 | $requestList->addElement( |
|||
92 | // Request |
||||
93 | 2 | Sequence::create([ |
|||
94 | // CertID [reqCert] |
||||
95 | 2 | Sequence::create([ |
|||
96 | // AlgorithmIdentifier [hashAlgorithm] |
||||
97 | 2 | $hashAlgorithm, |
|||
98 | // OCTET STRING [issuerNameHash] |
||||
99 | 2 | OctetString::create(sha1($request->getIssuerNameDer(), true)), |
|||
100 | // OCTET STRING [issuerKeyHash] |
||||
101 | 2 | OctetString::create(sha1($request->getIssuerPublicKeyBytes(), true)), |
|||
102 | // CertificateSerialNumber [serialNumber] |
||||
103 | 2 | Integer::create($request->getCertificateSerialNumber()), |
|||
104 | 2 | ]), |
|||
105 | 2 | ]) |
|||
106 | 2 | ); |
|||
107 | } |
||||
108 | |||||
109 | 2 | return $this->derEncoder->encodeElement( |
|||
110 | // OCSPRequest |
||||
111 | 2 | Sequence::create([ |
|||
112 | // TBSRequest [tbsRequest] |
||||
113 | 2 | Sequence::create([ |
|||
114 | 2 | $requestList, |
|||
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 | 2 | public function decodeOcspResponseSingle($rawResponseBody) |
|||
133 | { |
||||
134 | 2 | $responses = $this->decodeOcspResponse($rawResponseBody)->getResponses(); |
|||
135 | 2 | if (count($responses) !== 1) { |
|||
136 | throw ResponseException\MultipleResponsesException::create(); |
||||
137 | } |
||||
138 | |||||
139 | 2 | return $responses[0]; |
|||
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 | 2 | public function decodeOcspResponse($rawResponseBody) |
|||
155 | { |
||||
156 | 2 | $ocspResponse = $this->derDecoder->decodeElement($rawResponseBody); |
|||
157 | 2 | if (!$ocspResponse instanceof Sequence) { |
|||
158 | throw Asn1DecodingException::create('Invalid response type'); |
||||
159 | } |
||||
160 | 2 | $this->checkResponseStatus($ocspResponse); |
|||
161 | 2 | $responseBytes = $ocspResponse->getFirstChildOfType(0, Element::CLASS_CONTEXTSPECIFIC, Tag::ENVIRONMENT_EXPLICIT); |
|||
162 | 2 | if (!$responseBytes instanceof Sequence) { |
|||
163 | throw ResponseException\MissingResponseBytesException::create(); |
||||
164 | } |
||||
165 | |||||
166 | 2 | return $this->decodeResponseBytes($responseBytes); |
|||
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 | 2 | protected function checkResponseStatus(Sequence $ocspResponse) |
|||
180 | { |
||||
181 | 2 | $responseStatus = $ocspResponse->getFirstChildOfType(UniversalTagID::ENUMERATED); |
|||
182 | 2 | if ($responseStatus === null) { |
|||
183 | throw Asn1DecodingException::create('Invalid response type'); |
||||
184 | } |
||||
185 | 2 | switch ($responseStatus->getRawEncodedValue()) { |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
186 | 2 | case "\x00": // successful (Response has valid confirmations) |
|||
187 | 2 | break; |
|||
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 | 2 | protected function decodeResponseBytes(Sequence $responseBytes) |
|||
214 | { |
||||
215 | 2 | $responseType = $responseBytes->getFirstChildOfType(UniversalTagID::OBJECT_IDENTIFIER); |
|||
216 | 2 | $response = $responseBytes->getFirstChildOfType(UniversalTagID::OCTET_STRING); |
|||
217 | 2 | if ($responseType !== null && $response !== null) { |
|||
218 | 2 | switch ($responseType->getIdentifier()) { |
|||
0 ignored issues
–
show
The method
getIdentifier() does not exist on Ocsp\Asn1\Element . It seems like you code against a sub-type of Ocsp\Asn1\Element such as Ocsp\Asn1\Element\ObjectIdentifier .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
219 | 2 | case '1.3.6.1.5.5.7.48.1.1': |
|||
220 | 2 | return $this->decodeBasicResponse($response->getValue()); |
|||
0 ignored issues
–
show
The method
getValue() does not exist on Ocsp\Asn1\Element . It seems like you code against a sub-type of Ocsp\Asn1\Element such as Ocsp\Asn1\Element\PrintableString or Ocsp\Asn1\Element\Integer or Ocsp\Asn1\Element\GeneralizedTime or Ocsp\Asn1\Element\OctetString .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 | 2 | protected function decodeBasicResponse($responseBytes) |
|||
240 | { |
||||
241 | 2 | $basicOCSPResponse = $this->derDecoder->decodeElement($responseBytes); |
|||
242 | 2 | if (!$basicOCSPResponse instanceof Sequence) { |
|||
243 | throw Asn1DecodingException::create(); |
||||
244 | } |
||||
245 | 2 | $tbsResponseData = $basicOCSPResponse->getFirstChildOfType(UniversalTagID::SEQUENCE); |
|||
246 | 2 | if (!$tbsResponseData instanceof Sequence) { |
|||
247 | throw Asn1DecodingException::create(); |
||||
248 | } |
||||
249 | 2 | $responses = $tbsResponseData->getFirstChildOfType(UniversalTagID::SEQUENCE); |
|||
250 | 2 | if (!$responses instanceof Sequence) { |
|||
251 | throw Asn1DecodingException::create(); |
||||
252 | } |
||||
253 | 2 | $responseList = ResponseList::create(); |
|||
254 | 2 | foreach ($responses->getElements() as $singleResponse) { |
|||
255 | 2 | if ($singleResponse instanceof Sequence && $singleResponse->getTag() === null) { |
|||
256 | 2 | $responseList->addResponse($this->decodeBasicSingleResponse($singleResponse)); |
|||
257 | } |
||||
258 | } |
||||
259 | 2 | if ($responseList->getResponses() === []) { |
|||
260 | throw ResponseException\MissingResponseBytesException::create(); |
||||
261 | } |
||||
262 | |||||
263 | 2 | return $responseList; |
|||
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 | 2 | protected function decodeBasicSingleResponse(Sequence $singleResponse) |
|||
279 | { |
||||
280 | 2 | $elements = $singleResponse->getElements(); |
|||
281 | 2 | $certID = isset($elements[0]) ? $elements[0] : null; |
|||
282 | 2 | if (!$certID instanceof Sequence) { |
|||
283 | throw Asn1DecodingException::create(); |
||||
284 | } |
||||
285 | |||||
286 | 2 | $certificateSerialNumber = (string) $certID->getFirstChildOfType(UniversalTagID::INTEGER, Element::CLASS_UNIVERSAL)->getValue(); |
|||
287 | 2 | $thisUpdate = $singleResponse->getFirstChildOfType(UniversalTagID::GENERALIZEDTIME, Element::CLASS_UNIVERSAL)->getValue(); |
|||
288 | 2 | $nextUpdate = $singleResponse->getFirstChildOfType(0, Element::CLASS_CONTEXTSPECIFIC, Tag::ENVIRONMENT_EXPLICIT); |
|||
289 | 2 | if ($nextUpdate !== null) { |
|||
290 | 2 | $nextUpdate = $nextUpdate->getValue(); |
|||
291 | } |
||||
292 | |||||
293 | 2 | $certStatus = isset($elements[1]) ? $elements[1] : null; |
|||
294 | 2 | if ($certStatus === null) { |
|||
295 | throw Asn1DecodingException::create(); |
||||
296 | } |
||||
297 | 2 | $certStatusTag = $certStatus instanceof TaggableElement ? $certStatus->getTag() : null; |
|||
298 | 2 | if ($certStatusTag === null) { |
|||
299 | 1 | if ($certStatus->getClass() !== Element::CLASS_CONTEXTSPECIFIC) { |
|||
300 | throw Asn1DecodingException::create(); |
||||
301 | } |
||||
302 | 1 | $tagID = $certStatus->getTypeID(); |
|||
303 | } else { |
||||
304 | 1 | if ($certStatusTag->getClass() !== Element::CLASS_CONTEXTSPECIFIC) { |
|||
305 | throw Asn1DecodingException::create(); |
||||
306 | } |
||||
307 | 1 | $tagID = $certStatusTag->getTagID(); |
|||
308 | } |
||||
309 | switch ($tagID) { |
||||
310 | 2 | case 0: |
|||
311 | 1 | return Response::good($thisUpdate, $certificateSerialNumber, $nextUpdate); |
|||
312 | 1 | case 1: |
|||
313 | 1 | $revokedOn = null; |
|||
314 | 1 | $revocationReason = Response::REVOCATIONREASON_UNSPECIFIED; |
|||
315 | 1 | if ($certStatus instanceof GeneralizedTime) { |
|||
316 | 1 | $revokedOn = $certStatus->getValue(); |
|||
317 | } elseif ($certStatus instanceof AbstractList) { |
||||
318 | $certStatusChildren = $certStatus->getElements(); |
||||
319 | if (isset($certStatusChildren[0]) && $certStatusChildren[0] instanceof GeneralizedTime) { |
||||
320 | $revokedOn = $certStatusChildren[0]->getValue(); |
||||
321 | if (isset($certStatusChildren[1]) && $certStatusChildren[1] instanceof RawPrimitive) { |
||||
322 | $bitString = $certStatusChildren[1]->getRawEncodedValue(); |
||||
323 | if (strlen($bitString) === 1) { |
||||
324 | $revocationReason = ord($bitString[0]); |
||||
325 | } |
||||
326 | } |
||||
327 | } |
||||
328 | } |
||||
329 | 1 | if ($revokedOn === null) { |
|||
330 | throw Asn1DecodingException::create('Failed to find the revocation date/time'); |
||||
331 | } |
||||
332 | |||||
333 | 1 | return Response::revoked($thisUpdate, $certificateSerialNumber, $revokedOn, $revocationReason, $nextUpdate); |
|||
334 | case 2: |
||||
335 | default: |
||||
336 | return Response::unknown($thisUpdate, $certificateSerialNumber, $nextUpdate); |
||||
337 | } |
||||
338 | } |
||||
339 | } |
||||
340 |