Rev 1308 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Pure-PHP X.509 Parser |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
8 | * Encode and decode X.509 certificates. |
||
9 | * |
||
10 | * The extensions are from {@link http://tools.ietf.org/html/rfc5280 RFC5280} and |
||
11 | * {@link http://web.archive.org/web/19961027104704/http://www3.netscape.com/eng/security/cert-exts.html Netscape Certificate Extensions}. |
||
12 | * |
||
13 | * Note that loading an X.509 certificate and resaving it may invalidate the signature. The reason being that the signature is based on a |
||
14 | * portion of the certificate that contains optional parameters with default values. ie. if the parameter isn't there the default value is |
||
15 | * used. Problem is, if the parameter is there and it just so happens to have the default value there are two ways that that parameter can |
||
16 | * be encoded. It can be encoded explicitly or left out all together. This would effect the signature value and thus may invalidate the |
||
17 | * the certificate all together unless the certificate is re-signed. |
||
18 | * |
||
19 | * @author Jim Wigginton <terrafrost@php.net> |
||
20 | * @copyright 2012 Jim Wigginton |
||
21 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
22 | * @link http://phpseclib.sourceforge.net |
||
23 | */ |
||
24 | |||
25 | namespace phpseclib3\File; |
||
26 | |||
1042 | daniel-mar | 27 | use phpseclib3\Common\Functions\Strings; |
827 | daniel-mar | 28 | use phpseclib3\Crypt\Common\PrivateKey; |
29 | use phpseclib3\Crypt\Common\PublicKey; |
||
30 | use phpseclib3\Crypt\DSA; |
||
31 | use phpseclib3\Crypt\EC; |
||
32 | use phpseclib3\Crypt\Hash; |
||
33 | use phpseclib3\Crypt\PublicKeyLoader; |
||
34 | use phpseclib3\Crypt\Random; |
||
35 | use phpseclib3\Crypt\RSA; |
||
36 | use phpseclib3\Crypt\RSA\Formats\Keys\PSS; |
||
37 | use phpseclib3\Exception\UnsupportedAlgorithmException; |
||
38 | use phpseclib3\File\ASN1\Element; |
||
39 | use phpseclib3\File\ASN1\Maps; |
||
40 | use phpseclib3\Math\BigInteger; |
||
41 | |||
42 | /** |
||
43 | * Pure-PHP X.509 Parser |
||
44 | * |
||
45 | * @author Jim Wigginton <terrafrost@php.net> |
||
46 | */ |
||
47 | class X509 |
||
48 | { |
||
49 | /** |
||
50 | * Flag to only accept signatures signed by certificate authorities |
||
51 | * |
||
52 | * Not really used anymore but retained all the same to suppress E_NOTICEs from old installs |
||
53 | * |
||
54 | */ |
||
55 | const VALIDATE_SIGNATURE_BY_CA = 1; |
||
56 | |||
57 | /** |
||
58 | * Return internal array representation |
||
59 | * |
||
60 | * @see \phpseclib3\File\X509::getDN() |
||
61 | */ |
||
62 | const DN_ARRAY = 0; |
||
63 | /** |
||
64 | * Return string |
||
65 | * |
||
66 | * @see \phpseclib3\File\X509::getDN() |
||
67 | */ |
||
68 | const DN_STRING = 1; |
||
69 | /** |
||
70 | * Return ASN.1 name string |
||
71 | * |
||
72 | * @see \phpseclib3\File\X509::getDN() |
||
73 | */ |
||
74 | const DN_ASN1 = 2; |
||
75 | /** |
||
76 | * Return OpenSSL compatible array |
||
77 | * |
||
78 | * @see \phpseclib3\File\X509::getDN() |
||
79 | */ |
||
80 | const DN_OPENSSL = 3; |
||
81 | /** |
||
82 | * Return canonical ASN.1 RDNs string |
||
83 | * |
||
84 | * @see \phpseclib3\File\X509::getDN() |
||
85 | */ |
||
86 | const DN_CANON = 4; |
||
87 | /** |
||
88 | * Return name hash for file indexing |
||
89 | * |
||
90 | * @see \phpseclib3\File\X509::getDN() |
||
91 | */ |
||
92 | const DN_HASH = 5; |
||
93 | |||
94 | /** |
||
95 | * Save as PEM |
||
96 | * |
||
97 | * ie. a base64-encoded PEM with a header and a footer |
||
98 | * |
||
99 | * @see \phpseclib3\File\X509::saveX509() |
||
100 | * @see \phpseclib3\File\X509::saveCSR() |
||
101 | * @see \phpseclib3\File\X509::saveCRL() |
||
102 | */ |
||
103 | const FORMAT_PEM = 0; |
||
104 | /** |
||
105 | * Save as DER |
||
106 | * |
||
107 | * @see \phpseclib3\File\X509::saveX509() |
||
108 | * @see \phpseclib3\File\X509::saveCSR() |
||
109 | * @see \phpseclib3\File\X509::saveCRL() |
||
110 | */ |
||
111 | const FORMAT_DER = 1; |
||
112 | /** |
||
113 | * Save as a SPKAC |
||
114 | * |
||
115 | * @see \phpseclib3\File\X509::saveX509() |
||
116 | * @see \phpseclib3\File\X509::saveCSR() |
||
117 | * @see \phpseclib3\File\X509::saveCRL() |
||
118 | * |
||
119 | * Only works on CSRs. Not currently supported. |
||
120 | */ |
||
121 | const FORMAT_SPKAC = 2; |
||
122 | /** |
||
123 | * Auto-detect the format |
||
124 | * |
||
125 | * Used only by the load*() functions |
||
126 | * |
||
127 | * @see \phpseclib3\File\X509::saveX509() |
||
128 | * @see \phpseclib3\File\X509::saveCSR() |
||
129 | * @see \phpseclib3\File\X509::saveCRL() |
||
130 | */ |
||
131 | const FORMAT_AUTO_DETECT = 3; |
||
132 | |||
133 | /** |
||
134 | * Attribute value disposition. |
||
135 | * If disposition is >= 0, this is the index of the target value. |
||
136 | */ |
||
137 | const ATTR_ALL = -1; // All attribute values (array). |
||
138 | const ATTR_APPEND = -2; // Add a value. |
||
139 | const ATTR_REPLACE = -3; // Clear first, then add a value. |
||
140 | |||
141 | /** |
||
142 | * Distinguished Name |
||
143 | * |
||
144 | * @var array |
||
145 | */ |
||
146 | private $dn; |
||
147 | |||
148 | /** |
||
149 | * Public key |
||
150 | * |
||
151 | * @var string|PublicKey |
||
152 | */ |
||
153 | private $publicKey; |
||
154 | |||
155 | /** |
||
156 | * Private key |
||
157 | * |
||
158 | * @var string|PrivateKey |
||
159 | */ |
||
160 | private $privateKey; |
||
161 | |||
162 | /** |
||
163 | * The certificate authorities |
||
164 | * |
||
165 | * @var array |
||
166 | */ |
||
1308 | daniel-mar | 167 | private $CAs = []; |
827 | daniel-mar | 168 | |
169 | /** |
||
170 | * The currently loaded certificate |
||
171 | * |
||
172 | * @var array |
||
173 | */ |
||
174 | private $currentCert; |
||
175 | |||
176 | /** |
||
177 | * The signature subject |
||
178 | * |
||
179 | * There's no guarantee \phpseclib3\File\X509 is going to re-encode an X.509 cert in the same way it was originally |
||
180 | * encoded so we take save the portion of the original cert that the signature would have made for. |
||
181 | * |
||
182 | * @var string |
||
183 | */ |
||
184 | private $signatureSubject; |
||
185 | |||
186 | /** |
||
187 | * Certificate Start Date |
||
188 | * |
||
189 | * @var string |
||
190 | */ |
||
191 | private $startDate; |
||
192 | |||
193 | /** |
||
194 | * Certificate End Date |
||
195 | * |
||
196 | * @var string|Element |
||
197 | */ |
||
198 | private $endDate; |
||
199 | |||
200 | /** |
||
201 | * Serial Number |
||
202 | * |
||
203 | * @var string |
||
204 | */ |
||
205 | private $serialNumber; |
||
206 | |||
207 | /** |
||
208 | * Key Identifier |
||
209 | * |
||
210 | * See {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.1 RFC5280#section-4.2.1.1} and |
||
211 | * {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.2 RFC5280#section-4.2.1.2}. |
||
212 | * |
||
213 | * @var string |
||
214 | */ |
||
215 | private $currentKeyIdentifier; |
||
216 | |||
217 | /** |
||
218 | * CA Flag |
||
219 | * |
||
220 | * @var bool |
||
221 | */ |
||
222 | private $caFlag = false; |
||
223 | |||
224 | /** |
||
225 | * SPKAC Challenge |
||
226 | * |
||
227 | * @var string |
||
228 | */ |
||
229 | private $challenge; |
||
230 | |||
231 | /** |
||
232 | * @var array |
||
233 | */ |
||
234 | private $extensionValues = []; |
||
235 | |||
236 | /** |
||
237 | * OIDs loaded |
||
238 | * |
||
239 | * @var bool |
||
240 | */ |
||
241 | private static $oidsLoaded = false; |
||
242 | |||
243 | /** |
||
244 | * Recursion Limit |
||
245 | * |
||
246 | * @var int |
||
247 | */ |
||
248 | private static $recur_limit = 5; |
||
249 | |||
250 | /** |
||
251 | * URL fetch flag |
||
252 | * |
||
253 | * @var bool |
||
254 | */ |
||
255 | private static $disable_url_fetch = false; |
||
256 | |||
257 | /** |
||
258 | * @var array |
||
259 | */ |
||
260 | private static $extensions = []; |
||
261 | |||
262 | /** |
||
263 | * @var ?array |
||
264 | */ |
||
265 | private $ipAddresses = null; |
||
266 | |||
267 | /** |
||
268 | * @var ?array |
||
269 | */ |
||
270 | private $domains = null; |
||
271 | |||
272 | /** |
||
273 | * Default Constructor. |
||
274 | * |
||
275 | * @return \phpseclib3\File\X509 |
||
276 | */ |
||
277 | public function __construct() |
||
278 | { |
||
279 | // Explicitly Tagged Module, 1988 Syntax |
||
280 | // http://tools.ietf.org/html/rfc5280#appendix-A.1 |
||
281 | |||
282 | if (!self::$oidsLoaded) { |
||
283 | // OIDs from RFC5280 and those RFCs mentioned in RFC5280#section-4.1.1.2 |
||
284 | ASN1::loadOIDs([ |
||
285 | //'id-pkix' => '1.3.6.1.5.5.7', |
||
286 | //'id-pe' => '1.3.6.1.5.5.7.1', |
||
287 | //'id-qt' => '1.3.6.1.5.5.7.2', |
||
288 | //'id-kp' => '1.3.6.1.5.5.7.3', |
||
289 | //'id-ad' => '1.3.6.1.5.5.7.48', |
||
290 | 'id-qt-cps' => '1.3.6.1.5.5.7.2.1', |
||
291 | 'id-qt-unotice' => '1.3.6.1.5.5.7.2.2', |
||
292 | 'id-ad-ocsp' => '1.3.6.1.5.5.7.48.1', |
||
293 | 'id-ad-caIssuers' => '1.3.6.1.5.5.7.48.2', |
||
294 | 'id-ad-timeStamping' => '1.3.6.1.5.5.7.48.3', |
||
295 | 'id-ad-caRepository' => '1.3.6.1.5.5.7.48.5', |
||
296 | //'id-at' => '2.5.4', |
||
297 | 'id-at-name' => '2.5.4.41', |
||
298 | 'id-at-surname' => '2.5.4.4', |
||
299 | 'id-at-givenName' => '2.5.4.42', |
||
300 | 'id-at-initials' => '2.5.4.43', |
||
301 | 'id-at-generationQualifier' => '2.5.4.44', |
||
302 | 'id-at-commonName' => '2.5.4.3', |
||
303 | 'id-at-localityName' => '2.5.4.7', |
||
304 | 'id-at-stateOrProvinceName' => '2.5.4.8', |
||
305 | 'id-at-organizationName' => '2.5.4.10', |
||
306 | 'id-at-organizationalUnitName' => '2.5.4.11', |
||
307 | 'id-at-title' => '2.5.4.12', |
||
308 | 'id-at-description' => '2.5.4.13', |
||
309 | 'id-at-dnQualifier' => '2.5.4.46', |
||
310 | 'id-at-countryName' => '2.5.4.6', |
||
311 | 'id-at-serialNumber' => '2.5.4.5', |
||
312 | 'id-at-pseudonym' => '2.5.4.65', |
||
313 | 'id-at-postalCode' => '2.5.4.17', |
||
314 | 'id-at-streetAddress' => '2.5.4.9', |
||
315 | 'id-at-uniqueIdentifier' => '2.5.4.45', |
||
316 | 'id-at-role' => '2.5.4.72', |
||
317 | 'id-at-postalAddress' => '2.5.4.16', |
||
1308 | daniel-mar | 318 | 'jurisdictionOfIncorporationCountryName' => '1.3.6.1.4.1.311.60.2.1.3', |
319 | 'jurisdictionOfIncorporationStateOrProvinceName' => '1.3.6.1.4.1.311.60.2.1.2', |
||
320 | 'jurisdictionLocalityName' => '1.3.6.1.4.1.311.60.2.1.1', |
||
321 | 'id-at-businessCategory' => '2.5.4.15', |
||
827 | daniel-mar | 322 | |
323 | //'id-domainComponent' => '0.9.2342.19200300.100.1.25', |
||
324 | //'pkcs-9' => '1.2.840.113549.1.9', |
||
325 | 'pkcs-9-at-emailAddress' => '1.2.840.113549.1.9.1', |
||
326 | //'id-ce' => '2.5.29', |
||
327 | 'id-ce-authorityKeyIdentifier' => '2.5.29.35', |
||
328 | 'id-ce-subjectKeyIdentifier' => '2.5.29.14', |
||
329 | 'id-ce-keyUsage' => '2.5.29.15', |
||
330 | 'id-ce-privateKeyUsagePeriod' => '2.5.29.16', |
||
331 | 'id-ce-certificatePolicies' => '2.5.29.32', |
||
332 | //'anyPolicy' => '2.5.29.32.0', |
||
333 | |||
334 | 'id-ce-policyMappings' => '2.5.29.33', |
||
335 | |||
336 | 'id-ce-subjectAltName' => '2.5.29.17', |
||
337 | 'id-ce-issuerAltName' => '2.5.29.18', |
||
338 | 'id-ce-subjectDirectoryAttributes' => '2.5.29.9', |
||
339 | 'id-ce-basicConstraints' => '2.5.29.19', |
||
340 | 'id-ce-nameConstraints' => '2.5.29.30', |
||
341 | 'id-ce-policyConstraints' => '2.5.29.36', |
||
342 | 'id-ce-cRLDistributionPoints' => '2.5.29.31', |
||
343 | 'id-ce-extKeyUsage' => '2.5.29.37', |
||
344 | //'anyExtendedKeyUsage' => '2.5.29.37.0', |
||
345 | 'id-kp-serverAuth' => '1.3.6.1.5.5.7.3.1', |
||
346 | 'id-kp-clientAuth' => '1.3.6.1.5.5.7.3.2', |
||
347 | 'id-kp-codeSigning' => '1.3.6.1.5.5.7.3.3', |
||
348 | 'id-kp-emailProtection' => '1.3.6.1.5.5.7.3.4', |
||
349 | 'id-kp-timeStamping' => '1.3.6.1.5.5.7.3.8', |
||
350 | 'id-kp-OCSPSigning' => '1.3.6.1.5.5.7.3.9', |
||
351 | 'id-ce-inhibitAnyPolicy' => '2.5.29.54', |
||
352 | 'id-ce-freshestCRL' => '2.5.29.46', |
||
353 | 'id-pe-authorityInfoAccess' => '1.3.6.1.5.5.7.1.1', |
||
354 | 'id-pe-subjectInfoAccess' => '1.3.6.1.5.5.7.1.11', |
||
355 | 'id-ce-cRLNumber' => '2.5.29.20', |
||
356 | 'id-ce-issuingDistributionPoint' => '2.5.29.28', |
||
357 | 'id-ce-deltaCRLIndicator' => '2.5.29.27', |
||
358 | 'id-ce-cRLReasons' => '2.5.29.21', |
||
359 | 'id-ce-certificateIssuer' => '2.5.29.29', |
||
360 | 'id-ce-holdInstructionCode' => '2.5.29.23', |
||
361 | //'holdInstruction' => '1.2.840.10040.2', |
||
362 | 'id-holdinstruction-none' => '1.2.840.10040.2.1', |
||
363 | 'id-holdinstruction-callissuer' => '1.2.840.10040.2.2', |
||
364 | 'id-holdinstruction-reject' => '1.2.840.10040.2.3', |
||
365 | 'id-ce-invalidityDate' => '2.5.29.24', |
||
366 | |||
367 | 'rsaEncryption' => '1.2.840.113549.1.1.1', |
||
368 | 'md2WithRSAEncryption' => '1.2.840.113549.1.1.2', |
||
369 | 'md5WithRSAEncryption' => '1.2.840.113549.1.1.4', |
||
370 | 'sha1WithRSAEncryption' => '1.2.840.113549.1.1.5', |
||
371 | 'sha224WithRSAEncryption' => '1.2.840.113549.1.1.14', |
||
372 | 'sha256WithRSAEncryption' => '1.2.840.113549.1.1.11', |
||
373 | 'sha384WithRSAEncryption' => '1.2.840.113549.1.1.12', |
||
374 | 'sha512WithRSAEncryption' => '1.2.840.113549.1.1.13', |
||
375 | |||
376 | 'id-ecPublicKey' => '1.2.840.10045.2.1', |
||
377 | 'ecdsa-with-SHA1' => '1.2.840.10045.4.1', |
||
378 | // from https://tools.ietf.org/html/rfc5758#section-3.2 |
||
379 | 'ecdsa-with-SHA224' => '1.2.840.10045.4.3.1', |
||
380 | 'ecdsa-with-SHA256' => '1.2.840.10045.4.3.2', |
||
381 | 'ecdsa-with-SHA384' => '1.2.840.10045.4.3.3', |
||
382 | 'ecdsa-with-SHA512' => '1.2.840.10045.4.3.4', |
||
383 | |||
384 | 'id-dsa' => '1.2.840.10040.4.1', |
||
385 | 'id-dsa-with-sha1' => '1.2.840.10040.4.3', |
||
386 | // from https://tools.ietf.org/html/rfc5758#section-3.1 |
||
387 | 'id-dsa-with-sha224' => '2.16.840.1.101.3.4.3.1', |
||
388 | 'id-dsa-with-sha256' => '2.16.840.1.101.3.4.3.2', |
||
389 | |||
390 | // from https://tools.ietf.org/html/rfc8410: |
||
391 | 'id-Ed25519' => '1.3.101.112', |
||
392 | 'id-Ed448' => '1.3.101.113', |
||
393 | |||
394 | 'id-RSASSA-PSS' => '1.2.840.113549.1.1.10', |
||
395 | |||
396 | //'id-sha224' => '2.16.840.1.101.3.4.2.4', |
||
397 | //'id-sha256' => '2.16.840.1.101.3.4.2.1', |
||
398 | //'id-sha384' => '2.16.840.1.101.3.4.2.2', |
||
399 | //'id-sha512' => '2.16.840.1.101.3.4.2.3', |
||
400 | //'id-GostR3411-94-with-GostR3410-94' => '1.2.643.2.2.4', |
||
401 | //'id-GostR3411-94-with-GostR3410-2001' => '1.2.643.2.2.3', |
||
402 | //'id-GostR3410-2001' => '1.2.643.2.2.20', |
||
403 | //'id-GostR3410-94' => '1.2.643.2.2.19', |
||
404 | // Netscape Object Identifiers from "Netscape Certificate Extensions" |
||
405 | 'netscape' => '2.16.840.1.113730', |
||
406 | 'netscape-cert-extension' => '2.16.840.1.113730.1', |
||
407 | 'netscape-cert-type' => '2.16.840.1.113730.1.1', |
||
408 | 'netscape-comment' => '2.16.840.1.113730.1.13', |
||
409 | 'netscape-ca-policy-url' => '2.16.840.1.113730.1.8', |
||
410 | // the following are X.509 extensions not supported by phpseclib |
||
411 | 'id-pe-logotype' => '1.3.6.1.5.5.7.1.12', |
||
412 | 'entrustVersInfo' => '1.2.840.113533.7.65.0', |
||
413 | 'verisignPrivate' => '2.16.840.1.113733.1.6.9', |
||
414 | // for Certificate Signing Requests |
||
415 | // see http://tools.ietf.org/html/rfc2985 |
||
416 | 'pkcs-9-at-unstructuredName' => '1.2.840.113549.1.9.2', // PKCS #9 unstructured name |
||
417 | 'pkcs-9-at-challengePassword' => '1.2.840.113549.1.9.7', // Challenge password for certificate revocations |
||
418 | 'pkcs-9-at-extensionRequest' => '1.2.840.113549.1.9.14' // Certificate extension request |
||
419 | ]); |
||
420 | } |
||
421 | } |
||
422 | |||
423 | /** |
||
424 | * Load X.509 certificate |
||
425 | * |
||
426 | * Returns an associative array describing the X.509 cert or a false if the cert failed to load |
||
427 | * |
||
1042 | daniel-mar | 428 | * @param array|string $cert |
827 | daniel-mar | 429 | * @param int $mode |
430 | * @return mixed |
||
431 | */ |
||
432 | public function loadX509($cert, $mode = self::FORMAT_AUTO_DETECT) |
||
433 | { |
||
434 | if (is_array($cert) && isset($cert['tbsCertificate'])) { |
||
435 | unset($this->currentCert); |
||
436 | unset($this->currentKeyIdentifier); |
||
437 | $this->dn = $cert['tbsCertificate']['subject']; |
||
438 | if (!isset($this->dn)) { |
||
439 | return false; |
||
440 | } |
||
441 | $this->currentCert = $cert; |
||
442 | |||
443 | $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); |
||
444 | $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; |
||
445 | |||
446 | unset($this->signatureSubject); |
||
447 | |||
448 | return $cert; |
||
449 | } |
||
450 | |||
451 | if ($mode != self::FORMAT_DER) { |
||
452 | $newcert = ASN1::extractBER($cert); |
||
453 | if ($mode == self::FORMAT_PEM && $cert == $newcert) { |
||
454 | return false; |
||
455 | } |
||
456 | $cert = $newcert; |
||
457 | } |
||
458 | |||
459 | if ($cert === false) { |
||
460 | $this->currentCert = false; |
||
461 | return false; |
||
462 | } |
||
463 | |||
464 | $decoded = ASN1::decodeBER($cert); |
||
465 | |||
1042 | daniel-mar | 466 | if ($decoded) { |
827 | daniel-mar | 467 | $x509 = ASN1::asn1map($decoded[0], Maps\Certificate::MAP); |
468 | } |
||
469 | if (!isset($x509) || $x509 === false) { |
||
470 | $this->currentCert = false; |
||
471 | return false; |
||
472 | } |
||
473 | |||
474 | $this->signatureSubject = substr($cert, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); |
||
475 | |||
476 | if ($this->isSubArrayValid($x509, 'tbsCertificate/extensions')) { |
||
477 | $this->mapInExtensions($x509, 'tbsCertificate/extensions'); |
||
478 | } |
||
479 | $this->mapInDNs($x509, 'tbsCertificate/issuer/rdnSequence'); |
||
480 | $this->mapInDNs($x509, 'tbsCertificate/subject/rdnSequence'); |
||
481 | |||
482 | $key = $x509['tbsCertificate']['subjectPublicKeyInfo']; |
||
483 | $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); |
||
484 | $x509['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] = |
||
485 | "-----BEGIN PUBLIC KEY-----\r\n" . |
||
486 | chunk_split(base64_encode($key), 64) . |
||
487 | "-----END PUBLIC KEY-----"; |
||
488 | |||
489 | $this->currentCert = $x509; |
||
490 | $this->dn = $x509['tbsCertificate']['subject']; |
||
491 | |||
492 | $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); |
||
493 | $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; |
||
494 | |||
495 | return $x509; |
||
496 | } |
||
497 | |||
498 | /** |
||
499 | * Save X.509 certificate |
||
500 | * |
||
501 | * @param array $cert |
||
502 | * @param int $format optional |
||
503 | * @return string |
||
504 | */ |
||
1042 | daniel-mar | 505 | public function saveX509(array $cert, $format = self::FORMAT_PEM) |
827 | daniel-mar | 506 | { |
507 | if (!is_array($cert) || !isset($cert['tbsCertificate'])) { |
||
508 | return false; |
||
509 | } |
||
510 | |||
511 | switch (true) { |
||
512 | // "case !$a: case !$b: break; default: whatever();" is the same thing as "if ($a && $b) whatever()" |
||
513 | case !($algorithm = $this->subArray($cert, 'tbsCertificate/subjectPublicKeyInfo/algorithm/algorithm')): |
||
514 | case is_object($cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): |
||
515 | break; |
||
516 | default: |
||
517 | $cert['tbsCertificate']['subjectPublicKeyInfo'] = new Element( |
||
518 | base64_decode(preg_replace('#-.+-|[\r\n]#', '', $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'])) |
||
519 | ); |
||
520 | } |
||
521 | |||
522 | if ($algorithm == 'rsaEncryption') { |
||
523 | $cert['signatureAlgorithm']['parameters'] = null; |
||
524 | $cert['tbsCertificate']['signature']['parameters'] = null; |
||
525 | } |
||
526 | |||
527 | $filters = []; |
||
528 | $type_utf8_string = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
529 | $filters['tbsCertificate']['signature']['parameters'] = $type_utf8_string; |
||
530 | $filters['tbsCertificate']['signature']['issuer']['rdnSequence']['value'] = $type_utf8_string; |
||
531 | $filters['tbsCertificate']['issuer']['rdnSequence']['value'] = $type_utf8_string; |
||
532 | $filters['tbsCertificate']['subject']['rdnSequence']['value'] = $type_utf8_string; |
||
533 | $filters['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = $type_utf8_string; |
||
534 | $filters['signatureAlgorithm']['parameters'] = $type_utf8_string; |
||
535 | $filters['authorityCertIssuer']['directoryName']['rdnSequence']['value'] = $type_utf8_string; |
||
536 | //$filters['policyQualifiers']['qualifier'] = $type_utf8_string; |
||
537 | $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string; |
||
538 | $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string; |
||
539 | |||
540 | foreach (self::$extensions as $extension) { |
||
541 | $filters['tbsCertificate']['extensions'][] = $extension; |
||
542 | } |
||
543 | |||
544 | /* in the case of policyQualifiers/qualifier, the type has to be \phpseclib3\File\ASN1::TYPE_IA5_STRING. |
||
545 | \phpseclib3\File\ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random |
||
546 | characters. |
||
547 | */ |
||
548 | $filters['policyQualifiers']['qualifier'] |
||
549 | = ['type' => ASN1::TYPE_IA5_STRING]; |
||
550 | |||
551 | ASN1::setFilters($filters); |
||
552 | |||
553 | $this->mapOutExtensions($cert, 'tbsCertificate/extensions'); |
||
554 | $this->mapOutDNs($cert, 'tbsCertificate/issuer/rdnSequence'); |
||
555 | $this->mapOutDNs($cert, 'tbsCertificate/subject/rdnSequence'); |
||
556 | |||
557 | $cert = ASN1::encodeDER($cert, Maps\Certificate::MAP); |
||
558 | |||
559 | switch ($format) { |
||
560 | case self::FORMAT_DER: |
||
561 | return $cert; |
||
562 | // case self::FORMAT_PEM: |
||
563 | default: |
||
1042 | daniel-mar | 564 | return "-----BEGIN CERTIFICATE-----\r\n" . chunk_split(Strings::base64_encode($cert), 64) . '-----END CERTIFICATE-----'; |
827 | daniel-mar | 565 | } |
566 | } |
||
567 | |||
568 | /** |
||
569 | * Map extension values from octet string to extension-specific internal |
||
570 | * format. |
||
571 | * |
||
572 | * @param array $root (by reference) |
||
573 | * @param string $path |
||
574 | */ |
||
1042 | daniel-mar | 575 | private function mapInExtensions(array &$root, $path) |
827 | daniel-mar | 576 | { |
577 | $extensions = &$this->subArrayUnchecked($root, $path); |
||
578 | |||
579 | if ($extensions) { |
||
580 | for ($i = 0; $i < count($extensions); $i++) { |
||
581 | $id = $extensions[$i]['extnId']; |
||
582 | $value = &$extensions[$i]['extnValue']; |
||
583 | /* [extnValue] contains the DER encoding of an ASN.1 value |
||
584 | corresponding to the extension type identified by extnID */ |
||
585 | $map = $this->getMapping($id); |
||
586 | if (!is_bool($map)) { |
||
587 | $decoder = $id == 'id-ce-nameConstraints' ? |
||
588 | [static::class, 'decodeNameConstraintIP'] : |
||
589 | [static::class, 'decodeIP']; |
||
590 | $decoded = ASN1::decodeBER($value); |
||
1042 | daniel-mar | 591 | if (!$decoded) { |
592 | continue; |
||
593 | } |
||
827 | daniel-mar | 594 | $mapped = ASN1::asn1map($decoded[0], $map, ['iPAddress' => $decoder]); |
595 | $value = $mapped === false ? $decoded[0] : $mapped; |
||
596 | |||
597 | if ($id == 'id-ce-certificatePolicies') { |
||
598 | for ($j = 0; $j < count($value); $j++) { |
||
599 | if (!isset($value[$j]['policyQualifiers'])) { |
||
600 | continue; |
||
601 | } |
||
602 | for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { |
||
603 | $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; |
||
604 | $map = $this->getMapping($subid); |
||
605 | $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; |
||
606 | if ($map !== false) { |
||
607 | $decoded = ASN1::decodeBER($subvalue); |
||
1042 | daniel-mar | 608 | if (!$decoded) { |
609 | continue; |
||
610 | } |
||
827 | daniel-mar | 611 | $mapped = ASN1::asn1map($decoded[0], $map); |
612 | $subvalue = $mapped === false ? $decoded[0] : $mapped; |
||
613 | } |
||
614 | } |
||
615 | } |
||
616 | } |
||
617 | } |
||
618 | } |
||
619 | } |
||
620 | } |
||
621 | |||
622 | /** |
||
623 | * Map extension values from extension-specific internal format to |
||
624 | * octet string. |
||
625 | * |
||
626 | * @param array $root (by reference) |
||
627 | * @param string $path |
||
628 | */ |
||
1042 | daniel-mar | 629 | private function mapOutExtensions(array &$root, $path) |
827 | daniel-mar | 630 | { |
631 | $extensions = &$this->subArray($root, $path, !empty($this->extensionValues)); |
||
632 | |||
633 | foreach ($this->extensionValues as $id => $data) { |
||
634 | extract($data); |
||
635 | $newext = [ |
||
636 | 'extnId' => $id, |
||
637 | 'extnValue' => $value, |
||
638 | 'critical' => $critical |
||
639 | ]; |
||
640 | if ($replace) { |
||
641 | foreach ($extensions as $key => $value) { |
||
642 | if ($value['extnId'] == $id) { |
||
643 | $extensions[$key] = $newext; |
||
644 | continue 2; |
||
645 | } |
||
646 | } |
||
647 | } |
||
648 | $extensions[] = $newext; |
||
649 | } |
||
650 | |||
651 | if (is_array($extensions)) { |
||
652 | $size = count($extensions); |
||
653 | for ($i = 0; $i < $size; $i++) { |
||
654 | if ($extensions[$i] instanceof Element) { |
||
655 | continue; |
||
656 | } |
||
657 | |||
658 | $id = $extensions[$i]['extnId']; |
||
659 | $value = &$extensions[$i]['extnValue']; |
||
660 | |||
661 | switch ($id) { |
||
662 | case 'id-ce-certificatePolicies': |
||
663 | for ($j = 0; $j < count($value); $j++) { |
||
664 | if (!isset($value[$j]['policyQualifiers'])) { |
||
665 | continue; |
||
666 | } |
||
667 | for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { |
||
668 | $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; |
||
669 | $map = $this->getMapping($subid); |
||
670 | $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; |
||
671 | if ($map !== false) { |
||
672 | // by default \phpseclib3\File\ASN1 will try to render qualifier as a \phpseclib3\File\ASN1::TYPE_IA5_STRING since it's |
||
673 | // actual type is \phpseclib3\File\ASN1::TYPE_ANY |
||
674 | $subvalue = new Element(ASN1::encodeDER($subvalue, $map)); |
||
675 | } |
||
676 | } |
||
677 | } |
||
678 | break; |
||
679 | case 'id-ce-authorityKeyIdentifier': // use 00 as the serial number instead of an empty string |
||
680 | if (isset($value['authorityCertSerialNumber'])) { |
||
681 | if ($value['authorityCertSerialNumber']->toBytes() == '') { |
||
682 | $temp = chr((ASN1::CLASS_CONTEXT_SPECIFIC << 6) | 2) . "\1\0"; |
||
683 | $value['authorityCertSerialNumber'] = new Element($temp); |
||
684 | } |
||
685 | } |
||
686 | } |
||
687 | |||
688 | /* [extnValue] contains the DER encoding of an ASN.1 value |
||
689 | corresponding to the extension type identified by extnID */ |
||
690 | $map = $this->getMapping($id); |
||
691 | if (is_bool($map)) { |
||
692 | if (!$map) { |
||
693 | //user_error($id . ' is not a currently supported extension'); |
||
694 | unset($extensions[$i]); |
||
695 | } |
||
696 | } else { |
||
697 | $value = ASN1::encodeDER($value, $map, ['iPAddress' => [static::class, 'encodeIP']]); |
||
698 | } |
||
699 | } |
||
700 | } |
||
701 | } |
||
702 | |||
703 | /** |
||
704 | * Map attribute values from ANY type to attribute-specific internal |
||
705 | * format. |
||
706 | * |
||
707 | * @param array $root (by reference) |
||
708 | * @param string $path |
||
709 | */ |
||
710 | private function mapInAttributes(&$root, $path) |
||
711 | { |
||
712 | $attributes = &$this->subArray($root, $path); |
||
713 | |||
714 | if (is_array($attributes)) { |
||
715 | for ($i = 0; $i < count($attributes); $i++) { |
||
716 | $id = $attributes[$i]['type']; |
||
717 | /* $value contains the DER encoding of an ASN.1 value |
||
718 | corresponding to the attribute type identified by type */ |
||
719 | $map = $this->getMapping($id); |
||
720 | if (is_array($attributes[$i]['value'])) { |
||
721 | $values = &$attributes[$i]['value']; |
||
722 | for ($j = 0; $j < count($values); $j++) { |
||
723 | $value = ASN1::encodeDER($values[$j], Maps\AttributeValue::MAP); |
||
724 | $decoded = ASN1::decodeBER($value); |
||
725 | if (!is_bool($map)) { |
||
1042 | daniel-mar | 726 | if (!$decoded) { |
727 | continue; |
||
728 | } |
||
827 | daniel-mar | 729 | $mapped = ASN1::asn1map($decoded[0], $map); |
730 | if ($mapped !== false) { |
||
731 | $values[$j] = $mapped; |
||
732 | } |
||
733 | if ($id == 'pkcs-9-at-extensionRequest' && $this->isSubArrayValid($values, $j)) { |
||
734 | $this->mapInExtensions($values, $j); |
||
735 | } |
||
736 | } elseif ($map) { |
||
737 | $values[$j] = $value; |
||
738 | } |
||
739 | } |
||
740 | } |
||
741 | } |
||
742 | } |
||
743 | } |
||
744 | |||
745 | /** |
||
746 | * Map attribute values from attribute-specific internal format to |
||
747 | * ANY type. |
||
748 | * |
||
749 | * @param array $root (by reference) |
||
750 | * @param string $path |
||
751 | */ |
||
752 | private function mapOutAttributes(&$root, $path) |
||
753 | { |
||
754 | $attributes = &$this->subArray($root, $path); |
||
755 | |||
756 | if (is_array($attributes)) { |
||
757 | $size = count($attributes); |
||
758 | for ($i = 0; $i < $size; $i++) { |
||
759 | /* [value] contains the DER encoding of an ASN.1 value |
||
760 | corresponding to the attribute type identified by type */ |
||
761 | $id = $attributes[$i]['type']; |
||
762 | $map = $this->getMapping($id); |
||
763 | if ($map === false) { |
||
764 | //user_error($id . ' is not a currently supported attribute', E_USER_NOTICE); |
||
765 | unset($attributes[$i]); |
||
766 | } elseif (is_array($attributes[$i]['value'])) { |
||
767 | $values = &$attributes[$i]['value']; |
||
768 | for ($j = 0; $j < count($values); $j++) { |
||
769 | switch ($id) { |
||
770 | case 'pkcs-9-at-extensionRequest': |
||
771 | $this->mapOutExtensions($values, $j); |
||
772 | break; |
||
773 | } |
||
774 | |||
775 | if (!is_bool($map)) { |
||
776 | $temp = ASN1::encodeDER($values[$j], $map); |
||
777 | $decoded = ASN1::decodeBER($temp); |
||
1042 | daniel-mar | 778 | if (!$decoded) { |
779 | continue; |
||
780 | } |
||
827 | daniel-mar | 781 | $values[$j] = ASN1::asn1map($decoded[0], Maps\AttributeValue::MAP); |
782 | } |
||
783 | } |
||
784 | } |
||
785 | } |
||
786 | } |
||
787 | } |
||
788 | |||
789 | /** |
||
790 | * Map DN values from ANY type to DN-specific internal |
||
791 | * format. |
||
792 | * |
||
793 | * @param array $root (by reference) |
||
794 | * @param string $path |
||
795 | */ |
||
1042 | daniel-mar | 796 | private function mapInDNs(array &$root, $path) |
827 | daniel-mar | 797 | { |
798 | $dns = &$this->subArray($root, $path); |
||
799 | |||
800 | if (is_array($dns)) { |
||
801 | for ($i = 0; $i < count($dns); $i++) { |
||
802 | for ($j = 0; $j < count($dns[$i]); $j++) { |
||
803 | $type = $dns[$i][$j]['type']; |
||
804 | $value = &$dns[$i][$j]['value']; |
||
805 | if (is_object($value) && $value instanceof Element) { |
||
806 | $map = $this->getMapping($type); |
||
807 | if (!is_bool($map)) { |
||
808 | $decoded = ASN1::decodeBER($value); |
||
1042 | daniel-mar | 809 | if (!$decoded) { |
810 | continue; |
||
811 | } |
||
827 | daniel-mar | 812 | $value = ASN1::asn1map($decoded[0], $map); |
813 | } |
||
814 | } |
||
815 | } |
||
816 | } |
||
817 | } |
||
818 | } |
||
819 | |||
820 | /** |
||
821 | * Map DN values from DN-specific internal format to |
||
822 | * ANY type. |
||
823 | * |
||
824 | * @param array $root (by reference) |
||
825 | * @param string $path |
||
826 | */ |
||
1042 | daniel-mar | 827 | private function mapOutDNs(array &$root, $path) |
827 | daniel-mar | 828 | { |
829 | $dns = &$this->subArray($root, $path); |
||
830 | |||
831 | if (is_array($dns)) { |
||
832 | $size = count($dns); |
||
833 | for ($i = 0; $i < $size; $i++) { |
||
834 | for ($j = 0; $j < count($dns[$i]); $j++) { |
||
835 | $type = $dns[$i][$j]['type']; |
||
836 | $value = &$dns[$i][$j]['value']; |
||
837 | if (is_object($value) && $value instanceof Element) { |
||
838 | continue; |
||
839 | } |
||
840 | |||
841 | $map = $this->getMapping($type); |
||
842 | if (!is_bool($map)) { |
||
843 | $value = new Element(ASN1::encodeDER($value, $map)); |
||
844 | } |
||
845 | } |
||
846 | } |
||
847 | } |
||
848 | } |
||
849 | |||
850 | /** |
||
851 | * Associate an extension ID to an extension mapping |
||
852 | * |
||
853 | * @param string $extnId |
||
854 | * @return mixed |
||
855 | */ |
||
856 | private function getMapping($extnId) |
||
857 | { |
||
858 | if (!is_string($extnId)) { // eg. if it's a \phpseclib3\File\ASN1\Element object |
||
859 | return true; |
||
860 | } |
||
861 | |||
862 | if (isset(self::$extensions[$extnId])) { |
||
863 | return self::$extensions[$extnId]; |
||
864 | } |
||
865 | |||
866 | switch ($extnId) { |
||
867 | case 'id-ce-keyUsage': |
||
868 | return Maps\KeyUsage::MAP; |
||
869 | case 'id-ce-basicConstraints': |
||
870 | return Maps\BasicConstraints::MAP; |
||
871 | case 'id-ce-subjectKeyIdentifier': |
||
872 | return Maps\KeyIdentifier::MAP; |
||
873 | case 'id-ce-cRLDistributionPoints': |
||
874 | return Maps\CRLDistributionPoints::MAP; |
||
875 | case 'id-ce-authorityKeyIdentifier': |
||
876 | return Maps\AuthorityKeyIdentifier::MAP; |
||
877 | case 'id-ce-certificatePolicies': |
||
878 | return Maps\CertificatePolicies::MAP; |
||
879 | case 'id-ce-extKeyUsage': |
||
880 | return Maps\ExtKeyUsageSyntax::MAP; |
||
881 | case 'id-pe-authorityInfoAccess': |
||
882 | return Maps\AuthorityInfoAccessSyntax::MAP; |
||
883 | case 'id-ce-subjectAltName': |
||
884 | return Maps\SubjectAltName::MAP; |
||
885 | case 'id-ce-subjectDirectoryAttributes': |
||
886 | return Maps\SubjectDirectoryAttributes::MAP; |
||
887 | case 'id-ce-privateKeyUsagePeriod': |
||
888 | return Maps\PrivateKeyUsagePeriod::MAP; |
||
889 | case 'id-ce-issuerAltName': |
||
890 | return Maps\IssuerAltName::MAP; |
||
891 | case 'id-ce-policyMappings': |
||
892 | return Maps\PolicyMappings::MAP; |
||
893 | case 'id-ce-nameConstraints': |
||
894 | return Maps\NameConstraints::MAP; |
||
895 | |||
896 | case 'netscape-cert-type': |
||
897 | return Maps\netscape_cert_type::MAP; |
||
898 | case 'netscape-comment': |
||
899 | return Maps\netscape_comment::MAP; |
||
900 | case 'netscape-ca-policy-url': |
||
901 | return Maps\netscape_ca_policy_url::MAP; |
||
902 | |||
903 | // since id-qt-cps isn't a constructed type it will have already been decoded as a string by the time it gets |
||
904 | // back around to asn1map() and we don't want it decoded again. |
||
905 | //case 'id-qt-cps': |
||
906 | // return Maps\CPSuri::MAP; |
||
907 | case 'id-qt-unotice': |
||
908 | return Maps\UserNotice::MAP; |
||
909 | |||
910 | // the following OIDs are unsupported but we don't want them to give notices when calling saveX509(). |
||
911 | case 'id-pe-logotype': // http://www.ietf.org/rfc/rfc3709.txt |
||
912 | case 'entrustVersInfo': |
||
913 | // http://support.microsoft.com/kb/287547 |
||
914 | case '1.3.6.1.4.1.311.20.2': // szOID_ENROLL_CERTTYPE_EXTENSION |
||
915 | case '1.3.6.1.4.1.311.21.1': // szOID_CERTSRV_CA_VERSION |
||
916 | // "SET Secure Electronic Transaction Specification" |
||
917 | // http://www.maithean.com/docs/set_bk3.pdf |
||
918 | case '2.23.42.7.0': // id-set-hashedRootKey |
||
919 | // "Certificate Transparency" |
||
920 | // https://tools.ietf.org/html/rfc6962 |
||
921 | case '1.3.6.1.4.1.11129.2.4.2': |
||
922 | // "Qualified Certificate statements" |
||
923 | // https://tools.ietf.org/html/rfc3739#section-3.2.6 |
||
924 | case '1.3.6.1.5.5.7.1.3': |
||
925 | return true; |
||
926 | |||
927 | // CSR attributes |
||
928 | case 'pkcs-9-at-unstructuredName': |
||
929 | return Maps\PKCS9String::MAP; |
||
930 | case 'pkcs-9-at-challengePassword': |
||
931 | return Maps\DirectoryString::MAP; |
||
932 | case 'pkcs-9-at-extensionRequest': |
||
933 | return Maps\Extensions::MAP; |
||
934 | |||
935 | // CRL extensions. |
||
936 | case 'id-ce-cRLNumber': |
||
937 | return Maps\CRLNumber::MAP; |
||
938 | case 'id-ce-deltaCRLIndicator': |
||
939 | return Maps\CRLNumber::MAP; |
||
940 | case 'id-ce-issuingDistributionPoint': |
||
941 | return Maps\IssuingDistributionPoint::MAP; |
||
942 | case 'id-ce-freshestCRL': |
||
943 | return Maps\CRLDistributionPoints::MAP; |
||
944 | case 'id-ce-cRLReasons': |
||
945 | return Maps\CRLReason::MAP; |
||
946 | case 'id-ce-invalidityDate': |
||
947 | return Maps\InvalidityDate::MAP; |
||
948 | case 'id-ce-certificateIssuer': |
||
949 | return Maps\CertificateIssuer::MAP; |
||
950 | case 'id-ce-holdInstructionCode': |
||
951 | return Maps\HoldInstructionCode::MAP; |
||
952 | case 'id-at-postalAddress': |
||
953 | return Maps\PostalAddress::MAP; |
||
954 | } |
||
955 | |||
956 | return false; |
||
957 | } |
||
958 | |||
959 | /** |
||
960 | * Load an X.509 certificate as a certificate authority |
||
961 | * |
||
962 | * @param string $cert |
||
963 | * @return bool |
||
964 | */ |
||
965 | public function loadCA($cert) |
||
966 | { |
||
967 | $olddn = $this->dn; |
||
968 | $oldcert = $this->currentCert; |
||
969 | $oldsigsubj = $this->signatureSubject; |
||
970 | $oldkeyid = $this->currentKeyIdentifier; |
||
971 | |||
972 | $cert = $this->loadX509($cert); |
||
973 | if (!$cert) { |
||
974 | $this->dn = $olddn; |
||
975 | $this->currentCert = $oldcert; |
||
976 | $this->signatureSubject = $oldsigsubj; |
||
977 | $this->currentKeyIdentifier = $oldkeyid; |
||
978 | |||
979 | return false; |
||
980 | } |
||
981 | |||
982 | /* From RFC5280 "PKIX Certificate and CRL Profile": |
||
983 | |||
984 | If the keyUsage extension is present, then the subject public key |
||
985 | MUST NOT be used to verify signatures on certificates or CRLs unless |
||
986 | the corresponding keyCertSign or cRLSign bit is set. */ |
||
987 | //$keyUsage = $this->getExtension('id-ce-keyUsage'); |
||
988 | //if ($keyUsage && !in_array('keyCertSign', $keyUsage)) { |
||
989 | // return false; |
||
990 | //} |
||
991 | |||
992 | /* From RFC5280 "PKIX Certificate and CRL Profile": |
||
993 | |||
994 | The cA boolean indicates whether the certified public key may be used |
||
995 | to verify certificate signatures. If the cA boolean is not asserted, |
||
996 | then the keyCertSign bit in the key usage extension MUST NOT be |
||
997 | asserted. If the basic constraints extension is not present in a |
||
998 | version 3 certificate, or the extension is present but the cA boolean |
||
999 | is not asserted, then the certified public key MUST NOT be used to |
||
1000 | verify certificate signatures. */ |
||
1001 | //$basicConstraints = $this->getExtension('id-ce-basicConstraints'); |
||
1002 | //if (!$basicConstraints || !$basicConstraints['cA']) { |
||
1003 | // return false; |
||
1004 | //} |
||
1005 | |||
1006 | $this->CAs[] = $cert; |
||
1007 | |||
1008 | $this->dn = $olddn; |
||
1009 | $this->currentCert = $oldcert; |
||
1010 | $this->signatureSubject = $oldsigsubj; |
||
1011 | |||
1012 | return true; |
||
1013 | } |
||
1014 | |||
1015 | /** |
||
1016 | * Validate an X.509 certificate against a URL |
||
1017 | * |
||
1018 | * From RFC2818 "HTTP over TLS": |
||
1019 | * |
||
1020 | * Matching is performed using the matching rules specified by |
||
1021 | * [RFC2459]. If more than one identity of a given type is present in |
||
1022 | * the certificate (e.g., more than one dNSName name, a match in any one |
||
1023 | * of the set is considered acceptable.) Names may contain the wildcard |
||
1024 | * character * which is considered to match any single domain name |
||
1025 | * component or component fragment. E.g., *.a.com matches foo.a.com but |
||
1026 | * not bar.foo.a.com. f*.com matches foo.com but not bar.com. |
||
1027 | * |
||
1028 | * @param string $url |
||
1029 | * @return bool |
||
1030 | */ |
||
1031 | public function validateURL($url) |
||
1032 | { |
||
1033 | if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { |
||
1034 | return false; |
||
1035 | } |
||
1036 | |||
1037 | $components = parse_url($url); |
||
1038 | if (!isset($components['host'])) { |
||
1039 | return false; |
||
1040 | } |
||
1041 | |||
1042 | if ($names = $this->getExtension('id-ce-subjectAltName')) { |
||
1043 | foreach ($names as $name) { |
||
1044 | foreach ($name as $key => $value) { |
||
1411 | daniel-mar | 1045 | $value = preg_quote($value); |
1046 | $value = str_replace('\*', '[^.]*', $value); |
||
827 | daniel-mar | 1047 | switch ($key) { |
1048 | case 'dNSName': |
||
1049 | /* From RFC2818 "HTTP over TLS": |
||
1050 | |||
1051 | If a subjectAltName extension of type dNSName is present, that MUST |
||
1052 | be used as the identity. Otherwise, the (most specific) Common Name |
||
1053 | field in the Subject field of the certificate MUST be used. Although |
||
1054 | the use of the Common Name is existing practice, it is deprecated and |
||
1055 | Certification Authorities are encouraged to use the dNSName instead. */ |
||
1056 | if (preg_match('#^' . $value . '$#', $components['host'])) { |
||
1057 | return true; |
||
1058 | } |
||
1059 | break; |
||
1060 | case 'iPAddress': |
||
1061 | /* From RFC2818 "HTTP over TLS": |
||
1062 | |||
1063 | In some cases, the URI is specified as an IP address rather than a |
||
1064 | hostname. In this case, the iPAddress subjectAltName must be present |
||
1065 | in the certificate and must exactly match the IP in the URI. */ |
||
1066 | if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) { |
||
1067 | return true; |
||
1068 | } |
||
1069 | } |
||
1070 | } |
||
1071 | } |
||
1072 | return false; |
||
1073 | } |
||
1074 | |||
1075 | if ($value = $this->getDNProp('id-at-commonName')) { |
||
1076 | $value = str_replace(['.', '*'], ['\.', '[^.]*'], $value[0]); |
||
1077 | return preg_match('#^' . $value . '$#', $components['host']) === 1; |
||
1078 | } |
||
1079 | |||
1080 | return false; |
||
1081 | } |
||
1082 | |||
1083 | /** |
||
1084 | * Validate a date |
||
1085 | * |
||
1086 | * If $date isn't defined it is assumed to be the current date. |
||
1087 | * |
||
1088 | * @param \DateTimeInterface|string $date optional |
||
1089 | * @return bool |
||
1090 | */ |
||
1091 | public function validateDate($date = null) |
||
1092 | { |
||
1093 | if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { |
||
1094 | return false; |
||
1095 | } |
||
1096 | |||
1097 | if (!isset($date)) { |
||
1098 | $date = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); |
||
1099 | } |
||
1100 | |||
1101 | $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore']; |
||
1102 | $notBefore = isset($notBefore['generalTime']) ? $notBefore['generalTime'] : $notBefore['utcTime']; |
||
1103 | |||
1104 | $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter']; |
||
1105 | $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime']; |
||
1106 | |||
1107 | if (is_string($date)) { |
||
1108 | $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); |
||
1109 | } |
||
1110 | |||
1111 | $notBefore = new \DateTimeImmutable($notBefore, new \DateTimeZone(@date_default_timezone_get())); |
||
1112 | $notAfter = new \DateTimeImmutable($notAfter, new \DateTimeZone(@date_default_timezone_get())); |
||
1113 | |||
1114 | return $date >= $notBefore && $date <= $notAfter; |
||
1115 | } |
||
1116 | |||
1117 | /** |
||
1118 | * Fetches a URL |
||
1119 | * |
||
1120 | * @param string $url |
||
1121 | * @return bool|string |
||
1122 | */ |
||
1123 | private static function fetchURL($url) |
||
1124 | { |
||
1125 | if (self::$disable_url_fetch) { |
||
1126 | return false; |
||
1127 | } |
||
1128 | |||
1129 | $parts = parse_url($url); |
||
1130 | $data = ''; |
||
1131 | switch ($parts['scheme']) { |
||
1132 | case 'http': |
||
1133 | $fsock = @fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80); |
||
1134 | if (!$fsock) { |
||
1135 | return false; |
||
1136 | } |
||
1042 | daniel-mar | 1137 | $path = $parts['path']; |
1138 | if (isset($parts['query'])) { |
||
1139 | $path .= '?' . $parts['query']; |
||
1140 | } |
||
1141 | fputs($fsock, "GET $path HTTP/1.0\r\n"); |
||
827 | daniel-mar | 1142 | fputs($fsock, "Host: $parts[host]\r\n\r\n"); |
1143 | $line = fgets($fsock, 1024); |
||
1144 | if (strlen($line) < 3) { |
||
1145 | return false; |
||
1146 | } |
||
1147 | preg_match('#HTTP/1.\d (\d{3})#', $line, $temp); |
||
1148 | if ($temp[1] != '200') { |
||
1149 | return false; |
||
1150 | } |
||
1151 | |||
1152 | // skip the rest of the headers in the http response |
||
1153 | while (!feof($fsock) && fgets($fsock, 1024) != "\r\n") { |
||
1154 | } |
||
1155 | |||
1156 | while (!feof($fsock)) { |
||
1157 | $temp = fread($fsock, 1024); |
||
1158 | if ($temp === false) { |
||
1159 | return false; |
||
1160 | } |
||
1161 | $data .= $temp; |
||
1162 | } |
||
1163 | |||
1164 | break; |
||
1165 | //case 'ftp': |
||
1166 | //case 'ldap': |
||
1167 | //default: |
||
1168 | } |
||
1169 | |||
1170 | return $data; |
||
1171 | } |
||
1172 | |||
1173 | /** |
||
1174 | * Validates an intermediate cert as identified via authority info access extension |
||
1175 | * |
||
1176 | * See https://tools.ietf.org/html/rfc4325 for more info |
||
1177 | * |
||
1178 | * @param bool $caonly |
||
1179 | * @param int $count |
||
1180 | * @return bool |
||
1181 | */ |
||
1182 | private function testForIntermediate($caonly, $count) |
||
1183 | { |
||
1184 | $opts = $this->getExtension('id-pe-authorityInfoAccess'); |
||
1185 | if (!is_array($opts)) { |
||
1186 | return false; |
||
1187 | } |
||
1188 | foreach ($opts as $opt) { |
||
1189 | if ($opt['accessMethod'] == 'id-ad-caIssuers') { |
||
1190 | // accessLocation is a GeneralName. GeneralName fields support stuff like email addresses, IP addresses, LDAP, |
||
1191 | // etc, but we're only supporting URI's. URI's and LDAP are the only thing https://tools.ietf.org/html/rfc4325 |
||
1192 | // discusses |
||
1193 | if (isset($opt['accessLocation']['uniformResourceIdentifier'])) { |
||
1194 | $url = $opt['accessLocation']['uniformResourceIdentifier']; |
||
1195 | break; |
||
1196 | } |
||
1197 | } |
||
1198 | } |
||
1199 | |||
1200 | if (!isset($url)) { |
||
1201 | return false; |
||
1202 | } |
||
1203 | |||
1204 | $cert = static::fetchURL($url); |
||
1205 | if (!is_string($cert)) { |
||
1206 | return false; |
||
1207 | } |
||
1208 | |||
1209 | $parent = new static(); |
||
1210 | $parent->CAs = $this->CAs; |
||
1211 | /* |
||
1212 | "Conforming applications that support HTTP or FTP for accessing |
||
1213 | certificates MUST be able to accept .cer files and SHOULD be able |
||
1214 | to accept .p7c files." -- https://tools.ietf.org/html/rfc4325 |
||
1215 | |||
1216 | A .p7c file is 'a "certs-only" CMS message as specified in RFC 2797" |
||
1217 | |||
1218 | These are currently unsupported |
||
1219 | */ |
||
1220 | if (!is_array($parent->loadX509($cert))) { |
||
1221 | return false; |
||
1222 | } |
||
1223 | |||
1224 | if (!$parent->validateSignatureCountable($caonly, ++$count)) { |
||
1225 | return false; |
||
1226 | } |
||
1227 | |||
1228 | $this->CAs[] = $parent->currentCert; |
||
1229 | //$this->loadCA($cert); |
||
1230 | |||
1231 | return true; |
||
1232 | } |
||
1233 | |||
1234 | /** |
||
1235 | * Validate a signature |
||
1236 | * |
||
1237 | * Works on X.509 certs, CSR's and CRL's. |
||
1238 | * Returns true if the signature is verified, false if it is not correct or null on error |
||
1239 | * |
||
1240 | * By default returns false for self-signed certs. Call validateSignature(false) to make this support |
||
1241 | * self-signed. |
||
1242 | * |
||
1243 | * The behavior of this function is inspired by {@link http://php.net/openssl-verify openssl_verify}. |
||
1244 | * |
||
1245 | * @param bool $caonly optional |
||
1246 | * @return mixed |
||
1247 | */ |
||
1248 | public function validateSignature($caonly = true) |
||
1249 | { |
||
1250 | return $this->validateSignatureCountable($caonly, 0); |
||
1251 | } |
||
1252 | |||
1253 | /** |
||
1254 | * Validate a signature |
||
1255 | * |
||
1256 | * Performs said validation whilst keeping track of how many times validation method is called |
||
1257 | * |
||
1258 | * @param bool $caonly |
||
1259 | * @param int $count |
||
1260 | * @return mixed |
||
1261 | */ |
||
1262 | private function validateSignatureCountable($caonly, $count) |
||
1263 | { |
||
1264 | if (!is_array($this->currentCert) || !isset($this->signatureSubject)) { |
||
1265 | return null; |
||
1266 | } |
||
1267 | |||
1268 | if ($count == self::$recur_limit) { |
||
1269 | return false; |
||
1270 | } |
||
1271 | |||
1272 | /* TODO: |
||
1273 | "emailAddress attribute values are not case-sensitive (e.g., "subscriber@example.com" is the same as "SUBSCRIBER@EXAMPLE.COM")." |
||
1274 | -- http://tools.ietf.org/html/rfc5280#section-4.1.2.6 |
||
1275 | |||
1276 | implement pathLenConstraint in the id-ce-basicConstraints extension */ |
||
1277 | |||
1278 | switch (true) { |
||
1279 | case isset($this->currentCert['tbsCertificate']): |
||
1280 | // self-signed cert |
||
1281 | switch (true) { |
||
1282 | case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $this->currentCert['tbsCertificate']['subject']: |
||
1283 | case defined('FILE_X509_IGNORE_TYPE') && $this->getIssuerDN(self::DN_STRING) === $this->getDN(self::DN_STRING): |
||
1284 | $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); |
||
1285 | $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier'); |
||
1286 | switch (true) { |
||
1287 | case !is_array($authorityKey): |
||
1288 | case !$subjectKeyID: |
||
1289 | case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: |
||
1290 | $signingCert = $this->currentCert; // working cert |
||
1291 | } |
||
1292 | } |
||
1293 | |||
1294 | if (!empty($this->CAs)) { |
||
1295 | for ($i = 0; $i < count($this->CAs); $i++) { |
||
1296 | // even if the cert is a self-signed one we still want to see if it's a CA; |
||
1297 | // if not, we'll conditionally return an error |
||
1298 | $ca = $this->CAs[$i]; |
||
1299 | switch (true) { |
||
1300 | case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']: |
||
1301 | case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertificate']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): |
||
1302 | $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); |
||
1303 | $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); |
||
1304 | switch (true) { |
||
1305 | case !is_array($authorityKey): |
||
1306 | case !$subjectKeyID: |
||
1307 | case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: |
||
1308 | if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { |
||
1309 | break 2; // serial mismatch - check other ca |
||
1310 | } |
||
1311 | $signingCert = $ca; // working cert |
||
1312 | break 3; |
||
1313 | } |
||
1314 | } |
||
1315 | } |
||
1316 | if (count($this->CAs) == $i && $caonly) { |
||
1317 | return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly); |
||
1318 | } |
||
1319 | } elseif (!isset($signingCert) || $caonly) { |
||
1320 | return $this->testForIntermediate($caonly, $count) && $this->validateSignature($caonly); |
||
1321 | } |
||
1322 | return $this->validateSignatureHelper( |
||
1323 | $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], |
||
1324 | $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], |
||
1325 | $this->currentCert['signatureAlgorithm']['algorithm'], |
||
1326 | substr($this->currentCert['signature'], 1), |
||
1327 | $this->signatureSubject |
||
1328 | ); |
||
1329 | case isset($this->currentCert['certificationRequestInfo']): |
||
1330 | return $this->validateSignatureHelper( |
||
1331 | $this->currentCert['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm'], |
||
1332 | $this->currentCert['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], |
||
1333 | $this->currentCert['signatureAlgorithm']['algorithm'], |
||
1334 | substr($this->currentCert['signature'], 1), |
||
1335 | $this->signatureSubject |
||
1336 | ); |
||
1337 | case isset($this->currentCert['publicKeyAndChallenge']): |
||
1338 | return $this->validateSignatureHelper( |
||
1339 | $this->currentCert['publicKeyAndChallenge']['spki']['algorithm']['algorithm'], |
||
1340 | $this->currentCert['publicKeyAndChallenge']['spki']['subjectPublicKey'], |
||
1341 | $this->currentCert['signatureAlgorithm']['algorithm'], |
||
1342 | substr($this->currentCert['signature'], 1), |
||
1343 | $this->signatureSubject |
||
1344 | ); |
||
1345 | case isset($this->currentCert['tbsCertList']): |
||
1346 | if (!empty($this->CAs)) { |
||
1347 | for ($i = 0; $i < count($this->CAs); $i++) { |
||
1348 | $ca = $this->CAs[$i]; |
||
1349 | switch (true) { |
||
1350 | case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertList']['issuer'] === $ca['tbsCertificate']['subject']: |
||
1351 | case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertList']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): |
||
1352 | $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); |
||
1353 | $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); |
||
1354 | switch (true) { |
||
1355 | case !is_array($authorityKey): |
||
1356 | case !$subjectKeyID: |
||
1357 | case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: |
||
1358 | if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { |
||
1359 | break 2; // serial mismatch - check other ca |
||
1360 | } |
||
1361 | $signingCert = $ca; // working cert |
||
1362 | break 3; |
||
1363 | } |
||
1364 | } |
||
1365 | } |
||
1366 | } |
||
1367 | if (!isset($signingCert)) { |
||
1368 | return false; |
||
1369 | } |
||
1370 | return $this->validateSignatureHelper( |
||
1371 | $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], |
||
1372 | $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], |
||
1373 | $this->currentCert['signatureAlgorithm']['algorithm'], |
||
1374 | substr($this->currentCert['signature'], 1), |
||
1375 | $this->signatureSubject |
||
1376 | ); |
||
1377 | default: |
||
1378 | return false; |
||
1379 | } |
||
1380 | } |
||
1381 | |||
1382 | /** |
||
1383 | * Validates a signature |
||
1384 | * |
||
1385 | * Returns true if the signature is verified and false if it is not correct. |
||
1386 | * If the algorithms are unsupposed an exception is thrown. |
||
1387 | * |
||
1388 | * @param string $publicKeyAlgorithm |
||
1389 | * @param string $publicKey |
||
1390 | * @param string $signatureAlgorithm |
||
1391 | * @param string $signature |
||
1392 | * @param string $signatureSubject |
||
1393 | * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported |
||
1394 | * @return bool |
||
1395 | */ |
||
1396 | private function validateSignatureHelper($publicKeyAlgorithm, $publicKey, $signatureAlgorithm, $signature, $signatureSubject) |
||
1397 | { |
||
1398 | switch ($publicKeyAlgorithm) { |
||
1399 | case 'id-RSASSA-PSS': |
||
1400 | $key = RSA::loadFormat('PSS', $publicKey); |
||
1401 | break; |
||
1402 | case 'rsaEncryption': |
||
1403 | $key = RSA::loadFormat('PKCS8', $publicKey); |
||
1404 | switch ($signatureAlgorithm) { |
||
1042 | daniel-mar | 1405 | case 'id-RSASSA-PSS': |
1406 | break; |
||
827 | daniel-mar | 1407 | case 'md2WithRSAEncryption': |
1408 | case 'md5WithRSAEncryption': |
||
1409 | case 'sha1WithRSAEncryption': |
||
1410 | case 'sha224WithRSAEncryption': |
||
1411 | case 'sha256WithRSAEncryption': |
||
1412 | case 'sha384WithRSAEncryption': |
||
1413 | case 'sha512WithRSAEncryption': |
||
1414 | $key = $key |
||
1415 | ->withHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)) |
||
1416 | ->withPadding(RSA::SIGNATURE_PKCS1); |
||
1417 | break; |
||
1418 | default: |
||
1419 | throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); |
||
1420 | } |
||
1421 | break; |
||
1422 | case 'id-Ed25519': |
||
1423 | case 'id-Ed448': |
||
1424 | $key = EC::loadFormat('PKCS8', $publicKey); |
||
1425 | break; |
||
1426 | case 'id-ecPublicKey': |
||
1427 | $key = EC::loadFormat('PKCS8', $publicKey); |
||
1428 | switch ($signatureAlgorithm) { |
||
1429 | case 'ecdsa-with-SHA1': |
||
1430 | case 'ecdsa-with-SHA224': |
||
1431 | case 'ecdsa-with-SHA256': |
||
1432 | case 'ecdsa-with-SHA384': |
||
1433 | case 'ecdsa-with-SHA512': |
||
1434 | $key = $key |
||
1435 | ->withHash(preg_replace('#^ecdsa-with-#', '', strtolower($signatureAlgorithm))); |
||
1436 | break; |
||
1437 | default: |
||
1438 | throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); |
||
1439 | } |
||
1440 | break; |
||
1441 | case 'id-dsa': |
||
1442 | $key = DSA::loadFormat('PKCS8', $publicKey); |
||
1443 | switch ($signatureAlgorithm) { |
||
1444 | case 'id-dsa-with-sha1': |
||
1445 | case 'id-dsa-with-sha224': |
||
1446 | case 'id-dsa-with-sha256': |
||
1447 | $key = $key |
||
1448 | ->withHash(preg_replace('#^id-dsa-with-#', '', strtolower($signatureAlgorithm))); |
||
1449 | break; |
||
1450 | default: |
||
1451 | throw new UnsupportedAlgorithmException('Signature algorithm unsupported'); |
||
1452 | } |
||
1453 | break; |
||
1454 | default: |
||
1455 | throw new UnsupportedAlgorithmException('Public key algorithm unsupported'); |
||
1456 | } |
||
1457 | |||
1458 | return $key->verify($signatureSubject, $signature); |
||
1459 | } |
||
1460 | |||
1461 | /** |
||
1462 | * Sets the recursion limit |
||
1463 | * |
||
1464 | * When validating a signature it may be necessary to download intermediate certs from URI's. |
||
1465 | * An intermediate cert that linked to itself would result in an infinite loop so to prevent |
||
1466 | * that we set a recursion limit. A negative number means that there is no recursion limit. |
||
1467 | * |
||
1468 | * @param int $count |
||
1469 | */ |
||
1470 | public static function setRecurLimit($count) |
||
1471 | { |
||
1472 | self::$recur_limit = $count; |
||
1473 | } |
||
1474 | |||
1475 | /** |
||
1476 | * Prevents URIs from being automatically retrieved |
||
1477 | * |
||
1478 | */ |
||
1479 | public static function disableURLFetch() |
||
1480 | { |
||
1481 | self::$disable_url_fetch = true; |
||
1482 | } |
||
1483 | |||
1484 | /** |
||
1485 | * Allows URIs to be automatically retrieved |
||
1486 | * |
||
1487 | */ |
||
1488 | public static function enableURLFetch() |
||
1489 | { |
||
1490 | self::$disable_url_fetch = false; |
||
1491 | } |
||
1492 | |||
1493 | /** |
||
1494 | * Decodes an IP address |
||
1495 | * |
||
1496 | * Takes in a base64 encoded "blob" and returns a human readable IP address |
||
1497 | * |
||
1498 | * @param string $ip |
||
1499 | * @return string |
||
1500 | */ |
||
1501 | public static function decodeIP($ip) |
||
1502 | { |
||
1503 | return inet_ntop($ip); |
||
1504 | } |
||
1505 | |||
1506 | /** |
||
1507 | * Decodes an IP address in a name constraints extension |
||
1508 | * |
||
1509 | * Takes in a base64 encoded "blob" and returns a human readable IP address / mask |
||
1510 | * |
||
1511 | * @param string $ip |
||
1512 | * @return array |
||
1513 | */ |
||
1514 | public static function decodeNameConstraintIP($ip) |
||
1515 | { |
||
1516 | $size = strlen($ip) >> 1; |
||
1517 | $mask = substr($ip, $size); |
||
1518 | $ip = substr($ip, 0, $size); |
||
1519 | return [inet_ntop($ip), inet_ntop($mask)]; |
||
1520 | } |
||
1521 | |||
1522 | /** |
||
1523 | * Encodes an IP address |
||
1524 | * |
||
1525 | * Takes a human readable IP address into a base64-encoded "blob" |
||
1526 | * |
||
1527 | * @param string|array $ip |
||
1528 | * @return string |
||
1529 | */ |
||
1530 | public static function encodeIP($ip) |
||
1531 | { |
||
1532 | return is_string($ip) ? |
||
1533 | inet_pton($ip) : |
||
1534 | inet_pton($ip[0]) . inet_pton($ip[1]); |
||
1535 | } |
||
1536 | |||
1537 | /** |
||
1538 | * "Normalizes" a Distinguished Name property |
||
1539 | * |
||
1540 | * @param string $propName |
||
1541 | * @return mixed |
||
1542 | */ |
||
1543 | private function translateDNProp($propName) |
||
1544 | { |
||
1545 | switch (strtolower($propName)) { |
||
1308 | daniel-mar | 1546 | case 'jurisdictionofincorporationcountryname': |
1547 | case 'jurisdictioncountryname': |
||
1548 | case 'jurisdictionc': |
||
1549 | return 'jurisdictionOfIncorporationCountryName'; |
||
1550 | case 'jurisdictionofincorporationstateorprovincename': |
||
1551 | case 'jurisdictionstateorprovincename': |
||
1552 | case 'jurisdictionst': |
||
1553 | return 'jurisdictionOfIncorporationStateOrProvinceName'; |
||
1554 | case 'jurisdictionlocalityname': |
||
1555 | case 'jurisdictionl': |
||
1556 | return 'jurisdictionLocalityName'; |
||
1557 | case 'id-at-businesscategory': |
||
1558 | case 'businesscategory': |
||
1559 | return 'id-at-businessCategory'; |
||
827 | daniel-mar | 1560 | case 'id-at-countryname': |
1561 | case 'countryname': |
||
1562 | case 'c': |
||
1563 | return 'id-at-countryName'; |
||
1564 | case 'id-at-organizationname': |
||
1565 | case 'organizationname': |
||
1566 | case 'o': |
||
1567 | return 'id-at-organizationName'; |
||
1568 | case 'id-at-dnqualifier': |
||
1569 | case 'dnqualifier': |
||
1570 | return 'id-at-dnQualifier'; |
||
1571 | case 'id-at-commonname': |
||
1572 | case 'commonname': |
||
1573 | case 'cn': |
||
1574 | return 'id-at-commonName'; |
||
1575 | case 'id-at-stateorprovincename': |
||
1576 | case 'stateorprovincename': |
||
1577 | case 'state': |
||
1578 | case 'province': |
||
1579 | case 'provincename': |
||
1580 | case 'st': |
||
1581 | return 'id-at-stateOrProvinceName'; |
||
1582 | case 'id-at-localityname': |
||
1583 | case 'localityname': |
||
1584 | case 'l': |
||
1585 | return 'id-at-localityName'; |
||
1586 | case 'id-emailaddress': |
||
1587 | case 'emailaddress': |
||
1588 | return 'pkcs-9-at-emailAddress'; |
||
1589 | case 'id-at-serialnumber': |
||
1590 | case 'serialnumber': |
||
1591 | return 'id-at-serialNumber'; |
||
1592 | case 'id-at-postalcode': |
||
1593 | case 'postalcode': |
||
1594 | return 'id-at-postalCode'; |
||
1595 | case 'id-at-streetaddress': |
||
1596 | case 'streetaddress': |
||
1597 | return 'id-at-streetAddress'; |
||
1598 | case 'id-at-name': |
||
1599 | case 'name': |
||
1600 | return 'id-at-name'; |
||
1601 | case 'id-at-givenname': |
||
1602 | case 'givenname': |
||
1603 | return 'id-at-givenName'; |
||
1604 | case 'id-at-surname': |
||
1605 | case 'surname': |
||
1606 | case 'sn': |
||
1607 | return 'id-at-surname'; |
||
1608 | case 'id-at-initials': |
||
1609 | case 'initials': |
||
1610 | return 'id-at-initials'; |
||
1611 | case 'id-at-generationqualifier': |
||
1612 | case 'generationqualifier': |
||
1613 | return 'id-at-generationQualifier'; |
||
1614 | case 'id-at-organizationalunitname': |
||
1615 | case 'organizationalunitname': |
||
1616 | case 'ou': |
||
1617 | return 'id-at-organizationalUnitName'; |
||
1618 | case 'id-at-pseudonym': |
||
1619 | case 'pseudonym': |
||
1620 | return 'id-at-pseudonym'; |
||
1621 | case 'id-at-title': |
||
1622 | case 'title': |
||
1623 | return 'id-at-title'; |
||
1624 | case 'id-at-description': |
||
1625 | case 'description': |
||
1626 | return 'id-at-description'; |
||
1627 | case 'id-at-role': |
||
1628 | case 'role': |
||
1629 | return 'id-at-role'; |
||
1630 | case 'id-at-uniqueidentifier': |
||
1631 | case 'uniqueidentifier': |
||
1632 | case 'x500uniqueidentifier': |
||
1633 | return 'id-at-uniqueIdentifier'; |
||
1634 | case 'postaladdress': |
||
1635 | case 'id-at-postaladdress': |
||
1636 | return 'id-at-postalAddress'; |
||
1637 | default: |
||
1638 | return false; |
||
1639 | } |
||
1640 | } |
||
1641 | |||
1642 | /** |
||
1643 | * Set a Distinguished Name property |
||
1644 | * |
||
1645 | * @param string $propName |
||
1646 | * @param mixed $propValue |
||
1647 | * @param string $type optional |
||
1648 | * @return bool |
||
1649 | */ |
||
1650 | public function setDNProp($propName, $propValue, $type = 'utf8String') |
||
1651 | { |
||
1652 | if (empty($this->dn)) { |
||
1653 | $this->dn = ['rdnSequence' => []]; |
||
1654 | } |
||
1655 | |||
1656 | if (($propName = $this->translateDNProp($propName)) === false) { |
||
1657 | return false; |
||
1658 | } |
||
1659 | |||
1660 | foreach ((array) $propValue as $v) { |
||
1661 | if (!is_array($v) && isset($type)) { |
||
1662 | $v = [$type => $v]; |
||
1663 | } |
||
1664 | $this->dn['rdnSequence'][] = [ |
||
1665 | [ |
||
1666 | 'type' => $propName, |
||
1667 | 'value' => $v |
||
1668 | ] |
||
1669 | ]; |
||
1670 | } |
||
1671 | |||
1672 | return true; |
||
1673 | } |
||
1674 | |||
1675 | /** |
||
1676 | * Remove Distinguished Name properties |
||
1677 | * |
||
1678 | * @param string $propName |
||
1679 | */ |
||
1680 | public function removeDNProp($propName) |
||
1681 | { |
||
1682 | if (empty($this->dn)) { |
||
1683 | return; |
||
1684 | } |
||
1685 | |||
1686 | if (($propName = $this->translateDNProp($propName)) === false) { |
||
1687 | return; |
||
1688 | } |
||
1689 | |||
1690 | $dn = &$this->dn['rdnSequence']; |
||
1691 | $size = count($dn); |
||
1692 | for ($i = 0; $i < $size; $i++) { |
||
1693 | if ($dn[$i][0]['type'] == $propName) { |
||
1694 | unset($dn[$i]); |
||
1695 | } |
||
1696 | } |
||
1697 | |||
1698 | $dn = array_values($dn); |
||
1699 | // fix for https://bugs.php.net/75433 affecting PHP 7.2 |
||
1700 | if (!isset($dn[0])) { |
||
1701 | $dn = array_splice($dn, 0, 0); |
||
1702 | } |
||
1703 | } |
||
1704 | |||
1705 | /** |
||
1706 | * Get Distinguished Name properties |
||
1707 | * |
||
1708 | * @param string $propName |
||
1709 | * @param array $dn optional |
||
1710 | * @param bool $withType optional |
||
1711 | * @return mixed |
||
1712 | */ |
||
1042 | daniel-mar | 1713 | public function getDNProp($propName, array $dn = null, $withType = false) |
827 | daniel-mar | 1714 | { |
1715 | if (!isset($dn)) { |
||
1716 | $dn = $this->dn; |
||
1717 | } |
||
1718 | |||
1719 | if (empty($dn)) { |
||
1720 | return false; |
||
1721 | } |
||
1722 | |||
1723 | if (($propName = $this->translateDNProp($propName)) === false) { |
||
1724 | return false; |
||
1725 | } |
||
1726 | |||
1727 | $filters = []; |
||
1728 | $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
1729 | ASN1::setFilters($filters); |
||
1730 | $this->mapOutDNs($dn, 'rdnSequence'); |
||
1731 | $dn = $dn['rdnSequence']; |
||
1732 | $result = []; |
||
1733 | for ($i = 0; $i < count($dn); $i++) { |
||
1734 | if ($dn[$i][0]['type'] == $propName) { |
||
1735 | $v = $dn[$i][0]['value']; |
||
1736 | if (!$withType) { |
||
1737 | if (is_array($v)) { |
||
1738 | foreach ($v as $type => $s) { |
||
1739 | $type = array_search($type, ASN1::ANY_MAP); |
||
1740 | if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { |
||
1741 | $s = ASN1::convert($s, $type); |
||
1742 | if ($s !== false) { |
||
1743 | $v = $s; |
||
1744 | break; |
||
1745 | } |
||
1746 | } |
||
1747 | } |
||
1748 | if (is_array($v)) { |
||
1749 | $v = array_pop($v); // Always strip data type. |
||
1750 | } |
||
1751 | } elseif (is_object($v) && $v instanceof Element) { |
||
1752 | $map = $this->getMapping($propName); |
||
1753 | if (!is_bool($map)) { |
||
1754 | $decoded = ASN1::decodeBER($v); |
||
1042 | daniel-mar | 1755 | if (!$decoded) { |
1756 | return false; |
||
1757 | } |
||
827 | daniel-mar | 1758 | $v = ASN1::asn1map($decoded[0], $map); |
1759 | } |
||
1760 | } |
||
1761 | } |
||
1762 | $result[] = $v; |
||
1763 | } |
||
1764 | } |
||
1765 | |||
1766 | return $result; |
||
1767 | } |
||
1768 | |||
1769 | /** |
||
1770 | * Set a Distinguished Name |
||
1771 | * |
||
1772 | * @param mixed $dn |
||
1773 | * @param bool $merge optional |
||
1774 | * @param string $type optional |
||
1775 | * @return bool |
||
1776 | */ |
||
1777 | public function setDN($dn, $merge = false, $type = 'utf8String') |
||
1778 | { |
||
1779 | if (!$merge) { |
||
1780 | $this->dn = null; |
||
1781 | } |
||
1782 | |||
1783 | if (is_array($dn)) { |
||
1784 | if (isset($dn['rdnSequence'])) { |
||
1785 | $this->dn = $dn; // No merge here. |
||
1786 | return true; |
||
1787 | } |
||
1788 | |||
1789 | // handles stuff generated by openssl_x509_parse() |
||
1790 | foreach ($dn as $prop => $value) { |
||
1791 | if (!$this->setDNProp($prop, $value, $type)) { |
||
1792 | return false; |
||
1793 | } |
||
1794 | } |
||
1795 | return true; |
||
1796 | } |
||
1797 | |||
1798 | // handles everything else |
||
1799 | $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=|postalAddress=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); |
||
1800 | for ($i = 1; $i < count($results); $i += 2) { |
||
1801 | $prop = trim($results[$i], ', =/'); |
||
1802 | $value = $results[$i + 1]; |
||
1803 | if (!$this->setDNProp($prop, $value, $type)) { |
||
1804 | return false; |
||
1805 | } |
||
1806 | } |
||
1807 | |||
1808 | return true; |
||
1809 | } |
||
1810 | |||
1811 | /** |
||
1812 | * Get the Distinguished Name for a certificates subject |
||
1813 | * |
||
1814 | * @param mixed $format optional |
||
1815 | * @param array $dn optional |
||
1816 | * @return array|bool|string |
||
1817 | */ |
||
1042 | daniel-mar | 1818 | public function getDN($format = self::DN_ARRAY, array $dn = null) |
827 | daniel-mar | 1819 | { |
1820 | if (!isset($dn)) { |
||
1821 | $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn; |
||
1822 | } |
||
1823 | |||
1824 | switch ((int) $format) { |
||
1825 | case self::DN_ARRAY: |
||
1826 | return $dn; |
||
1827 | case self::DN_ASN1: |
||
1828 | $filters = []; |
||
1829 | $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
1830 | ASN1::setFilters($filters); |
||
1831 | $this->mapOutDNs($dn, 'rdnSequence'); |
||
1832 | return ASN1::encodeDER($dn, Maps\Name::MAP); |
||
1833 | case self::DN_CANON: |
||
1834 | // No SEQUENCE around RDNs and all string values normalized as |
||
1835 | // trimmed lowercase UTF-8 with all spacing as one blank. |
||
1836 | // constructed RDNs will not be canonicalized |
||
1837 | $filters = []; |
||
1838 | $filters['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
1839 | ASN1::setFilters($filters); |
||
1840 | $result = ''; |
||
1841 | $this->mapOutDNs($dn, 'rdnSequence'); |
||
1842 | foreach ($dn['rdnSequence'] as $rdn) { |
||
1843 | foreach ($rdn as $i => $attr) { |
||
1844 | $attr = &$rdn[$i]; |
||
1845 | if (is_array($attr['value'])) { |
||
1846 | foreach ($attr['value'] as $type => $v) { |
||
1847 | $type = array_search($type, ASN1::ANY_MAP, true); |
||
1848 | if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { |
||
1849 | $v = ASN1::convert($v, $type); |
||
1850 | if ($v !== false) { |
||
1851 | $v = preg_replace('/\s+/', ' ', $v); |
||
1852 | $attr['value'] = strtolower(trim($v)); |
||
1853 | break; |
||
1854 | } |
||
1855 | } |
||
1856 | } |
||
1857 | } |
||
1858 | } |
||
1859 | $result .= ASN1::encodeDER($rdn, Maps\RelativeDistinguishedName::MAP); |
||
1860 | } |
||
1861 | return $result; |
||
1862 | case self::DN_HASH: |
||
1863 | $dn = $this->getDN(self::DN_CANON, $dn); |
||
1864 | $hash = new Hash('sha1'); |
||
1865 | $hash = $hash->hash($dn); |
||
1866 | extract(unpack('Vhash', $hash)); |
||
1042 | daniel-mar | 1867 | return strtolower(Strings::bin2hex(pack('N', $hash))); |
827 | daniel-mar | 1868 | } |
1869 | |||
1870 | // Default is to return a string. |
||
1871 | $start = true; |
||
1872 | $output = ''; |
||
1873 | |||
1874 | $result = []; |
||
1875 | $filters = []; |
||
1876 | $filters['rdnSequence']['value'] = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
1877 | ASN1::setFilters($filters); |
||
1878 | $this->mapOutDNs($dn, 'rdnSequence'); |
||
1879 | |||
1880 | foreach ($dn['rdnSequence'] as $field) { |
||
1881 | $prop = $field[0]['type']; |
||
1882 | $value = $field[0]['value']; |
||
1883 | |||
1884 | $delim = ', '; |
||
1885 | switch ($prop) { |
||
1886 | case 'id-at-countryName': |
||
1887 | $desc = 'C'; |
||
1888 | break; |
||
1889 | case 'id-at-stateOrProvinceName': |
||
1890 | $desc = 'ST'; |
||
1891 | break; |
||
1892 | case 'id-at-organizationName': |
||
1893 | $desc = 'O'; |
||
1894 | break; |
||
1895 | case 'id-at-organizationalUnitName': |
||
1896 | $desc = 'OU'; |
||
1897 | break; |
||
1898 | case 'id-at-commonName': |
||
1899 | $desc = 'CN'; |
||
1900 | break; |
||
1901 | case 'id-at-localityName': |
||
1902 | $desc = 'L'; |
||
1903 | break; |
||
1904 | case 'id-at-surname': |
||
1905 | $desc = 'SN'; |
||
1906 | break; |
||
1907 | case 'id-at-uniqueIdentifier': |
||
1908 | $delim = '/'; |
||
1909 | $desc = 'x500UniqueIdentifier'; |
||
1910 | break; |
||
1911 | case 'id-at-postalAddress': |
||
1912 | $delim = '/'; |
||
1913 | $desc = 'postalAddress'; |
||
1914 | break; |
||
1915 | default: |
||
1916 | $delim = '/'; |
||
1917 | $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop); |
||
1918 | } |
||
1919 | |||
1920 | if (!$start) { |
||
1921 | $output .= $delim; |
||
1922 | } |
||
1923 | if (is_array($value)) { |
||
1924 | foreach ($value as $type => $v) { |
||
1925 | $type = array_search($type, ASN1::ANY_MAP, true); |
||
1926 | if ($type !== false && array_key_exists($type, ASN1::STRING_TYPE_SIZE)) { |
||
1927 | $v = ASN1::convert($v, $type); |
||
1928 | if ($v !== false) { |
||
1929 | $value = $v; |
||
1930 | break; |
||
1931 | } |
||
1932 | } |
||
1933 | } |
||
1934 | if (is_array($value)) { |
||
1935 | $value = array_pop($value); // Always strip data type. |
||
1936 | } |
||
1937 | } elseif (is_object($value) && $value instanceof Element) { |
||
1938 | $callback = function ($x) { |
||
1939 | return '\x' . bin2hex($x[0]); |
||
1940 | }; |
||
1941 | $value = strtoupper(preg_replace_callback('#[^\x20-\x7E]#', $callback, $value->element)); |
||
1942 | } |
||
1943 | $output .= $desc . '=' . $value; |
||
1944 | $result[$desc] = isset($result[$desc]) ? |
||
1945 | array_merge((array) $result[$desc], [$value]) : |
||
1946 | $value; |
||
1947 | $start = false; |
||
1948 | } |
||
1949 | |||
1950 | return $format == self::DN_OPENSSL ? $result : $output; |
||
1951 | } |
||
1952 | |||
1953 | /** |
||
1954 | * Get the Distinguished Name for a certificate/crl issuer |
||
1955 | * |
||
1956 | * @param int $format optional |
||
1957 | * @return mixed |
||
1958 | */ |
||
1959 | public function getIssuerDN($format = self::DN_ARRAY) |
||
1960 | { |
||
1961 | switch (true) { |
||
1962 | case !isset($this->currentCert) || !is_array($this->currentCert): |
||
1963 | break; |
||
1964 | case isset($this->currentCert['tbsCertificate']): |
||
1965 | return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']); |
||
1966 | case isset($this->currentCert['tbsCertList']): |
||
1967 | return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']); |
||
1968 | } |
||
1969 | |||
1970 | return false; |
||
1971 | } |
||
1972 | |||
1973 | /** |
||
1974 | * Get the Distinguished Name for a certificate/csr subject |
||
1975 | * Alias of getDN() |
||
1976 | * |
||
1977 | * @param int $format optional |
||
1978 | * @return mixed |
||
1979 | */ |
||
1980 | public function getSubjectDN($format = self::DN_ARRAY) |
||
1981 | { |
||
1982 | switch (true) { |
||
1983 | case !empty($this->dn): |
||
1984 | return $this->getDN($format); |
||
1985 | case !isset($this->currentCert) || !is_array($this->currentCert): |
||
1986 | break; |
||
1987 | case isset($this->currentCert['tbsCertificate']): |
||
1988 | return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']); |
||
1989 | case isset($this->currentCert['certificationRequestInfo']): |
||
1990 | return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']); |
||
1991 | } |
||
1992 | |||
1993 | return false; |
||
1994 | } |
||
1995 | |||
1996 | /** |
||
1997 | * Get an individual Distinguished Name property for a certificate/crl issuer |
||
1998 | * |
||
1999 | * @param string $propName |
||
2000 | * @param bool $withType optional |
||
2001 | * @return mixed |
||
2002 | */ |
||
2003 | public function getIssuerDNProp($propName, $withType = false) |
||
2004 | { |
||
2005 | switch (true) { |
||
2006 | case !isset($this->currentCert) || !is_array($this->currentCert): |
||
2007 | break; |
||
2008 | case isset($this->currentCert['tbsCertificate']): |
||
2009 | return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType); |
||
2010 | case isset($this->currentCert['tbsCertList']): |
||
2011 | return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType); |
||
2012 | } |
||
2013 | |||
2014 | return false; |
||
2015 | } |
||
2016 | |||
2017 | /** |
||
2018 | * Get an individual Distinguished Name property for a certificate/csr subject |
||
2019 | * |
||
2020 | * @param string $propName |
||
2021 | * @param bool $withType optional |
||
2022 | * @return mixed |
||
2023 | */ |
||
2024 | public function getSubjectDNProp($propName, $withType = false) |
||
2025 | { |
||
2026 | switch (true) { |
||
2027 | case !empty($this->dn): |
||
2028 | return $this->getDNProp($propName, null, $withType); |
||
2029 | case !isset($this->currentCert) || !is_array($this->currentCert): |
||
2030 | break; |
||
2031 | case isset($this->currentCert['tbsCertificate']): |
||
2032 | return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType); |
||
2033 | case isset($this->currentCert['certificationRequestInfo']): |
||
2034 | return $this->getDNProp($propName, $this->currentCert['certificationRequestInfo']['subject'], $withType); |
||
2035 | } |
||
2036 | |||
2037 | return false; |
||
2038 | } |
||
2039 | |||
2040 | /** |
||
2041 | * Get the certificate chain for the current cert |
||
2042 | * |
||
2043 | * @return mixed |
||
2044 | */ |
||
2045 | public function getChain() |
||
2046 | { |
||
2047 | $chain = [$this->currentCert]; |
||
2048 | |||
2049 | if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { |
||
2050 | return false; |
||
2051 | } |
||
2052 | while (true) { |
||
2053 | $currentCert = $chain[count($chain) - 1]; |
||
2054 | for ($i = 0; $i < count($this->CAs); $i++) { |
||
2055 | $ca = $this->CAs[$i]; |
||
2056 | if ($currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']) { |
||
2057 | $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier', $currentCert); |
||
2058 | $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); |
||
2059 | switch (true) { |
||
2060 | case !is_array($authorityKey): |
||
2061 | case is_array($authorityKey) && isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: |
||
2062 | if ($currentCert === $ca) { |
||
2063 | break 3; |
||
2064 | } |
||
2065 | $chain[] = $ca; |
||
2066 | break 2; |
||
2067 | } |
||
2068 | } |
||
2069 | } |
||
2070 | if ($i == count($this->CAs)) { |
||
2071 | break; |
||
2072 | } |
||
2073 | } |
||
2074 | foreach ($chain as $key => $value) { |
||
2075 | $chain[$key] = new X509(); |
||
2076 | $chain[$key]->loadX509($value); |
||
2077 | } |
||
2078 | return $chain; |
||
2079 | } |
||
2080 | |||
2081 | /** |
||
2082 | * Returns the current cert |
||
2083 | * |
||
2084 | * @return array|bool |
||
2085 | */ |
||
2086 | public function &getCurrentCert() |
||
2087 | { |
||
2088 | return $this->currentCert; |
||
2089 | } |
||
2090 | |||
2091 | /** |
||
2092 | * Set public key |
||
2093 | * |
||
2094 | * Key needs to be a \phpseclib3\Crypt\RSA object |
||
2095 | * |
||
2096 | * @param PublicKey $key |
||
2097 | * @return void |
||
2098 | */ |
||
2099 | public function setPublicKey(PublicKey $key) |
||
2100 | { |
||
2101 | $this->publicKey = $key; |
||
2102 | } |
||
2103 | |||
2104 | /** |
||
2105 | * Set private key |
||
2106 | * |
||
2107 | * Key needs to be a \phpseclib3\Crypt\RSA object |
||
2108 | * |
||
2109 | * @param PrivateKey $key |
||
2110 | */ |
||
2111 | public function setPrivateKey(PrivateKey $key) |
||
2112 | { |
||
2113 | $this->privateKey = $key; |
||
2114 | } |
||
2115 | |||
2116 | /** |
||
2117 | * Set challenge |
||
2118 | * |
||
2119 | * Used for SPKAC CSR's |
||
2120 | * |
||
2121 | * @param string $challenge |
||
2122 | */ |
||
2123 | public function setChallenge($challenge) |
||
2124 | { |
||
2125 | $this->challenge = $challenge; |
||
2126 | } |
||
2127 | |||
2128 | /** |
||
2129 | * Gets the public key |
||
2130 | * |
||
2131 | * Returns a \phpseclib3\Crypt\RSA object or a false. |
||
2132 | * |
||
2133 | * @return mixed |
||
2134 | */ |
||
2135 | public function getPublicKey() |
||
2136 | { |
||
2137 | if (isset($this->publicKey)) { |
||
2138 | return $this->publicKey; |
||
2139 | } |
||
2140 | |||
2141 | if (isset($this->currentCert) && is_array($this->currentCert)) { |
||
2142 | $paths = [ |
||
2143 | 'tbsCertificate/subjectPublicKeyInfo', |
||
2144 | 'certificationRequestInfo/subjectPKInfo', |
||
2145 | 'publicKeyAndChallenge/spki' |
||
2146 | ]; |
||
2147 | foreach ($paths as $path) { |
||
2148 | $keyinfo = $this->subArray($this->currentCert, $path); |
||
2149 | if (!empty($keyinfo)) { |
||
2150 | break; |
||
2151 | } |
||
2152 | } |
||
2153 | } |
||
2154 | if (empty($keyinfo)) { |
||
2155 | return false; |
||
2156 | } |
||
2157 | |||
2158 | $key = $keyinfo['subjectPublicKey']; |
||
2159 | |||
2160 | switch ($keyinfo['algorithm']['algorithm']) { |
||
2161 | case 'id-RSASSA-PSS': |
||
2162 | return RSA::loadFormat('PSS', $key); |
||
2163 | case 'rsaEncryption': |
||
2164 | return RSA::loadFormat('PKCS8', $key)->withPadding(RSA::SIGNATURE_PKCS1); |
||
2165 | case 'id-ecPublicKey': |
||
2166 | case 'id-Ed25519': |
||
2167 | case 'id-Ed448': |
||
2168 | return EC::loadFormat('PKCS8', $key); |
||
2169 | case 'id-dsa': |
||
2170 | return DSA::loadFormat('PKCS8', $key); |
||
2171 | } |
||
2172 | |||
2173 | return false; |
||
2174 | } |
||
2175 | |||
2176 | /** |
||
2177 | * Load a Certificate Signing Request |
||
2178 | * |
||
2179 | * @param string $csr |
||
2180 | * @param int $mode |
||
2181 | * @return mixed |
||
2182 | */ |
||
2183 | public function loadCSR($csr, $mode = self::FORMAT_AUTO_DETECT) |
||
2184 | { |
||
2185 | if (is_array($csr) && isset($csr['certificationRequestInfo'])) { |
||
2186 | unset($this->currentCert); |
||
2187 | unset($this->currentKeyIdentifier); |
||
2188 | unset($this->signatureSubject); |
||
2189 | $this->dn = $csr['certificationRequestInfo']['subject']; |
||
2190 | if (!isset($this->dn)) { |
||
2191 | return false; |
||
2192 | } |
||
2193 | |||
2194 | $this->currentCert = $csr; |
||
2195 | return $csr; |
||
2196 | } |
||
2197 | |||
2198 | // see http://tools.ietf.org/html/rfc2986 |
||
2199 | |||
2200 | if ($mode != self::FORMAT_DER) { |
||
2201 | $newcsr = ASN1::extractBER($csr); |
||
2202 | if ($mode == self::FORMAT_PEM && $csr == $newcsr) { |
||
2203 | return false; |
||
2204 | } |
||
2205 | $csr = $newcsr; |
||
2206 | } |
||
2207 | $orig = $csr; |
||
2208 | |||
2209 | if ($csr === false) { |
||
2210 | $this->currentCert = false; |
||
2211 | return false; |
||
2212 | } |
||
2213 | |||
2214 | $decoded = ASN1::decodeBER($csr); |
||
2215 | |||
1042 | daniel-mar | 2216 | if (!$decoded) { |
827 | daniel-mar | 2217 | $this->currentCert = false; |
2218 | return false; |
||
2219 | } |
||
2220 | |||
2221 | $csr = ASN1::asn1map($decoded[0], Maps\CertificationRequest::MAP); |
||
2222 | if (!isset($csr) || $csr === false) { |
||
2223 | $this->currentCert = false; |
||
2224 | return false; |
||
2225 | } |
||
2226 | |||
2227 | $this->mapInAttributes($csr, 'certificationRequestInfo/attributes'); |
||
2228 | $this->mapInDNs($csr, 'certificationRequestInfo/subject/rdnSequence'); |
||
2229 | |||
2230 | $this->dn = $csr['certificationRequestInfo']['subject']; |
||
2231 | |||
2232 | $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); |
||
2233 | |||
2234 | $key = $csr['certificationRequestInfo']['subjectPKInfo']; |
||
2235 | $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); |
||
2236 | $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'] = |
||
2237 | "-----BEGIN PUBLIC KEY-----\r\n" . |
||
2238 | chunk_split(base64_encode($key), 64) . |
||
2239 | "-----END PUBLIC KEY-----"; |
||
2240 | |||
2241 | $this->currentKeyIdentifier = null; |
||
2242 | $this->currentCert = $csr; |
||
2243 | |||
2244 | $this->publicKey = null; |
||
2245 | $this->publicKey = $this->getPublicKey(); |
||
2246 | |||
2247 | return $csr; |
||
2248 | } |
||
2249 | |||
2250 | /** |
||
2251 | * Save CSR request |
||
2252 | * |
||
2253 | * @param array $csr |
||
2254 | * @param int $format optional |
||
2255 | * @return string |
||
2256 | */ |
||
1042 | daniel-mar | 2257 | public function saveCSR(array $csr, $format = self::FORMAT_PEM) |
827 | daniel-mar | 2258 | { |
2259 | if (!is_array($csr) || !isset($csr['certificationRequestInfo'])) { |
||
2260 | return false; |
||
2261 | } |
||
2262 | |||
2263 | switch (true) { |
||
2264 | case !($algorithm = $this->subArray($csr, 'certificationRequestInfo/subjectPKInfo/algorithm/algorithm')): |
||
2265 | case is_object($csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): |
||
2266 | break; |
||
2267 | default: |
||
2268 | $csr['certificationRequestInfo']['subjectPKInfo'] = new Element( |
||
2269 | base64_decode(preg_replace('#-.+-|[\r\n]#', '', $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'])) |
||
2270 | ); |
||
2271 | } |
||
2272 | |||
2273 | $filters = []; |
||
2274 | $filters['certificationRequestInfo']['subject']['rdnSequence']['value'] |
||
2275 | = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
2276 | |||
2277 | ASN1::setFilters($filters); |
||
2278 | |||
2279 | $this->mapOutDNs($csr, 'certificationRequestInfo/subject/rdnSequence'); |
||
2280 | $this->mapOutAttributes($csr, 'certificationRequestInfo/attributes'); |
||
2281 | $csr = ASN1::encodeDER($csr, Maps\CertificationRequest::MAP); |
||
2282 | |||
2283 | switch ($format) { |
||
2284 | case self::FORMAT_DER: |
||
2285 | return $csr; |
||
2286 | // case self::FORMAT_PEM: |
||
2287 | default: |
||
1042 | daniel-mar | 2288 | return "-----BEGIN CERTIFICATE REQUEST-----\r\n" . chunk_split(Strings::base64_encode($csr), 64) . '-----END CERTIFICATE REQUEST-----'; |
827 | daniel-mar | 2289 | } |
2290 | } |
||
2291 | |||
2292 | /** |
||
2293 | * Load a SPKAC CSR |
||
2294 | * |
||
2295 | * SPKAC's are produced by the HTML5 keygen element: |
||
2296 | * |
||
2297 | * https://developer.mozilla.org/en-US/docs/HTML/Element/keygen |
||
2298 | * |
||
2299 | * @param string $spkac |
||
2300 | * @return mixed |
||
2301 | */ |
||
2302 | public function loadSPKAC($spkac) |
||
2303 | { |
||
2304 | if (is_array($spkac) && isset($spkac['publicKeyAndChallenge'])) { |
||
2305 | unset($this->currentCert); |
||
2306 | unset($this->currentKeyIdentifier); |
||
2307 | unset($this->signatureSubject); |
||
2308 | $this->currentCert = $spkac; |
||
2309 | return $spkac; |
||
2310 | } |
||
2311 | |||
2312 | // see http://www.w3.org/html/wg/drafts/html/master/forms.html#signedpublickeyandchallenge |
||
2313 | |||
2314 | // OpenSSL produces SPKAC's that are preceded by the string SPKAC= |
||
2315 | $temp = preg_replace('#(?:SPKAC=)|[ \r\n\\\]#', '', $spkac); |
||
1042 | daniel-mar | 2316 | $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? Strings::base64_decode($temp) : false; |
827 | daniel-mar | 2317 | if ($temp != false) { |
2318 | $spkac = $temp; |
||
2319 | } |
||
2320 | $orig = $spkac; |
||
2321 | |||
2322 | if ($spkac === false) { |
||
2323 | $this->currentCert = false; |
||
2324 | return false; |
||
2325 | } |
||
2326 | |||
2327 | $decoded = ASN1::decodeBER($spkac); |
||
2328 | |||
1042 | daniel-mar | 2329 | if (!$decoded) { |
827 | daniel-mar | 2330 | $this->currentCert = false; |
2331 | return false; |
||
2332 | } |
||
2333 | |||
2334 | $spkac = ASN1::asn1map($decoded[0], Maps\SignedPublicKeyAndChallenge::MAP); |
||
2335 | |||
2336 | if (!isset($spkac) || !is_array($spkac)) { |
||
2337 | $this->currentCert = false; |
||
2338 | return false; |
||
2339 | } |
||
2340 | |||
2341 | $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); |
||
2342 | |||
2343 | $key = $spkac['publicKeyAndChallenge']['spki']; |
||
2344 | $key = ASN1::encodeDER($key, Maps\SubjectPublicKeyInfo::MAP); |
||
2345 | $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'] = |
||
2346 | "-----BEGIN PUBLIC KEY-----\r\n" . |
||
2347 | chunk_split(base64_encode($key), 64) . |
||
2348 | "-----END PUBLIC KEY-----"; |
||
2349 | |||
2350 | $this->currentKeyIdentifier = null; |
||
2351 | $this->currentCert = $spkac; |
||
2352 | |||
2353 | $this->publicKey = null; |
||
2354 | $this->publicKey = $this->getPublicKey(); |
||
2355 | |||
2356 | return $spkac; |
||
2357 | } |
||
2358 | |||
2359 | /** |
||
2360 | * Save a SPKAC CSR request |
||
2361 | * |
||
2362 | * @param array $spkac |
||
2363 | * @param int $format optional |
||
2364 | * @return string |
||
2365 | */ |
||
1042 | daniel-mar | 2366 | public function saveSPKAC(array $spkac, $format = self::FORMAT_PEM) |
827 | daniel-mar | 2367 | { |
2368 | if (!is_array($spkac) || !isset($spkac['publicKeyAndChallenge'])) { |
||
2369 | return false; |
||
2370 | } |
||
2371 | |||
2372 | $algorithm = $this->subArray($spkac, 'publicKeyAndChallenge/spki/algorithm/algorithm'); |
||
2373 | switch (true) { |
||
2374 | case !$algorithm: |
||
2375 | case is_object($spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']): |
||
2376 | break; |
||
2377 | default: |
||
2378 | $spkac['publicKeyAndChallenge']['spki'] = new Element( |
||
2379 | base64_decode(preg_replace('#-.+-|[\r\n]#', '', $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'])) |
||
2380 | ); |
||
2381 | } |
||
2382 | |||
2383 | $spkac = ASN1::encodeDER($spkac, Maps\SignedPublicKeyAndChallenge::MAP); |
||
2384 | |||
2385 | switch ($format) { |
||
2386 | case self::FORMAT_DER: |
||
2387 | return $spkac; |
||
2388 | // case self::FORMAT_PEM: |
||
2389 | default: |
||
2390 | // OpenSSL's implementation of SPKAC requires the SPKAC be preceded by SPKAC= and since there are pretty much |
||
2391 | // no other SPKAC decoders phpseclib will use that same format |
||
1042 | daniel-mar | 2392 | return 'SPKAC=' . Strings::base64_encode($spkac); |
827 | daniel-mar | 2393 | } |
2394 | } |
||
2395 | |||
2396 | /** |
||
2397 | * Load a Certificate Revocation List |
||
2398 | * |
||
2399 | * @param string $crl |
||
2400 | * @param int $mode |
||
2401 | * @return mixed |
||
2402 | */ |
||
2403 | public function loadCRL($crl, $mode = self::FORMAT_AUTO_DETECT) |
||
2404 | { |
||
2405 | if (is_array($crl) && isset($crl['tbsCertList'])) { |
||
2406 | $this->currentCert = $crl; |
||
2407 | unset($this->signatureSubject); |
||
2408 | return $crl; |
||
2409 | } |
||
2410 | |||
2411 | if ($mode != self::FORMAT_DER) { |
||
2412 | $newcrl = ASN1::extractBER($crl); |
||
2413 | if ($mode == self::FORMAT_PEM && $crl == $newcrl) { |
||
2414 | return false; |
||
2415 | } |
||
2416 | $crl = $newcrl; |
||
2417 | } |
||
2418 | $orig = $crl; |
||
2419 | |||
2420 | if ($crl === false) { |
||
2421 | $this->currentCert = false; |
||
2422 | return false; |
||
2423 | } |
||
2424 | |||
2425 | $decoded = ASN1::decodeBER($crl); |
||
2426 | |||
1042 | daniel-mar | 2427 | if (!$decoded) { |
827 | daniel-mar | 2428 | $this->currentCert = false; |
2429 | return false; |
||
2430 | } |
||
2431 | |||
2432 | $crl = ASN1::asn1map($decoded[0], Maps\CertificateList::MAP); |
||
2433 | if (!isset($crl) || $crl === false) { |
||
2434 | $this->currentCert = false; |
||
2435 | return false; |
||
2436 | } |
||
2437 | |||
2438 | $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); |
||
2439 | |||
2440 | $this->mapInDNs($crl, 'tbsCertList/issuer/rdnSequence'); |
||
2441 | if ($this->isSubArrayValid($crl, 'tbsCertList/crlExtensions')) { |
||
2442 | $this->mapInExtensions($crl, 'tbsCertList/crlExtensions'); |
||
2443 | } |
||
2444 | if ($this->isSubArrayValid($crl, 'tbsCertList/revokedCertificates')) { |
||
2445 | $rclist_ref = &$this->subArrayUnchecked($crl, 'tbsCertList/revokedCertificates'); |
||
2446 | if ($rclist_ref) { |
||
2447 | $rclist = $crl['tbsCertList']['revokedCertificates']; |
||
2448 | foreach ($rclist as $i => $extension) { |
||
2449 | if ($this->isSubArrayValid($rclist, "$i/crlEntryExtensions")) { |
||
2450 | $this->mapInExtensions($rclist_ref, "$i/crlEntryExtensions"); |
||
2451 | } |
||
2452 | } |
||
2453 | } |
||
2454 | } |
||
2455 | |||
2456 | $this->currentKeyIdentifier = null; |
||
2457 | $this->currentCert = $crl; |
||
2458 | |||
2459 | return $crl; |
||
2460 | } |
||
2461 | |||
2462 | /** |
||
2463 | * Save Certificate Revocation List. |
||
2464 | * |
||
2465 | * @param array $crl |
||
2466 | * @param int $format optional |
||
2467 | * @return string |
||
2468 | */ |
||
1042 | daniel-mar | 2469 | public function saveCRL(array $crl, $format = self::FORMAT_PEM) |
827 | daniel-mar | 2470 | { |
2471 | if (!is_array($crl) || !isset($crl['tbsCertList'])) { |
||
2472 | return false; |
||
2473 | } |
||
2474 | |||
2475 | $filters = []; |
||
2476 | $filters['tbsCertList']['issuer']['rdnSequence']['value'] |
||
2477 | = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
2478 | $filters['tbsCertList']['signature']['parameters'] |
||
2479 | = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
2480 | $filters['signatureAlgorithm']['parameters'] |
||
2481 | = ['type' => ASN1::TYPE_UTF8_STRING]; |
||
2482 | |||
2483 | if (empty($crl['tbsCertList']['signature']['parameters'])) { |
||
2484 | $filters['tbsCertList']['signature']['parameters'] |
||
2485 | = ['type' => ASN1::TYPE_NULL]; |
||
2486 | } |
||
2487 | |||
2488 | if (empty($crl['signatureAlgorithm']['parameters'])) { |
||
2489 | $filters['signatureAlgorithm']['parameters'] |
||
2490 | = ['type' => ASN1::TYPE_NULL]; |
||
2491 | } |
||
2492 | |||
2493 | ASN1::setFilters($filters); |
||
2494 | |||
2495 | $this->mapOutDNs($crl, 'tbsCertList/issuer/rdnSequence'); |
||
2496 | $this->mapOutExtensions($crl, 'tbsCertList/crlExtensions'); |
||
2497 | $rclist = &$this->subArray($crl, 'tbsCertList/revokedCertificates'); |
||
2498 | if (is_array($rclist)) { |
||
2499 | foreach ($rclist as $i => $extension) { |
||
2500 | $this->mapOutExtensions($rclist, "$i/crlEntryExtensions"); |
||
2501 | } |
||
2502 | } |
||
2503 | |||
2504 | $crl = ASN1::encodeDER($crl, Maps\CertificateList::MAP); |
||
2505 | |||
2506 | switch ($format) { |
||
2507 | case self::FORMAT_DER: |
||
2508 | return $crl; |
||
2509 | // case self::FORMAT_PEM: |
||
2510 | default: |
||
1042 | daniel-mar | 2511 | return "-----BEGIN X509 CRL-----\r\n" . chunk_split(Strings::base64_encode($crl), 64) . '-----END X509 CRL-----'; |
827 | daniel-mar | 2512 | } |
2513 | } |
||
2514 | |||
2515 | /** |
||
2516 | * Helper function to build a time field according to RFC 3280 section |
||
2517 | * - 4.1.2.5 Validity |
||
2518 | * - 5.1.2.4 This Update |
||
2519 | * - 5.1.2.5 Next Update |
||
2520 | * - 5.1.2.6 Revoked Certificates |
||
2521 | * by choosing utcTime iff year of date given is before 2050 and generalTime else. |
||
2522 | * |
||
2523 | * @param string $date in format date('D, d M Y H:i:s O') |
||
2524 | * @return array|Element |
||
2525 | */ |
||
2526 | private function timeField($date) |
||
2527 | { |
||
2528 | if ($date instanceof Element) { |
||
2529 | return $date; |
||
2530 | } |
||
2531 | $dateObj = new \DateTimeImmutable($date, new \DateTimeZone('GMT')); |
||
2532 | $year = $dateObj->format('Y'); // the same way ASN1.php parses this |
||
2533 | if ($year < 2050) { |
||
2534 | return ['utcTime' => $date]; |
||
2535 | } else { |
||
2536 | return ['generalTime' => $date]; |
||
2537 | } |
||
2538 | } |
||
2539 | |||
2540 | /** |
||
2541 | * Sign an X.509 certificate |
||
2542 | * |
||
2543 | * $issuer's private key needs to be loaded. |
||
2544 | * $subject can be either an existing X.509 cert (if you want to resign it), |
||
2545 | * a CSR or something with the DN and public key explicitly set. |
||
2546 | * |
||
2547 | * @return mixed |
||
2548 | */ |
||
1042 | daniel-mar | 2549 | public function sign(X509 $issuer, X509 $subject) |
827 | daniel-mar | 2550 | { |
2551 | if (!is_object($issuer->privateKey) || empty($issuer->dn)) { |
||
2552 | return false; |
||
2553 | } |
||
2554 | |||
2555 | if (isset($subject->publicKey) && !($subjectPublicKey = $subject->formatSubjectPublicKey())) { |
||
2556 | return false; |
||
2557 | } |
||
2558 | |||
2559 | $currentCert = isset($this->currentCert) ? $this->currentCert : null; |
||
2560 | $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; |
||
2561 | $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey); |
||
2562 | |||
2563 | if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) { |
||
2564 | $this->currentCert = $subject->currentCert; |
||
2565 | $this->currentCert['tbsCertificate']['signature'] = $signatureAlgorithm; |
||
2566 | $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; |
||
2567 | |||
2568 | if (!empty($this->startDate)) { |
||
2569 | $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->timeField($this->startDate); |
||
2570 | } |
||
2571 | if (!empty($this->endDate)) { |
||
2572 | $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->timeField($this->endDate); |
||
2573 | } |
||
2574 | if (!empty($this->serialNumber)) { |
||
2575 | $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber; |
||
2576 | } |
||
2577 | if (!empty($subject->dn)) { |
||
2578 | $this->currentCert['tbsCertificate']['subject'] = $subject->dn; |
||
2579 | } |
||
2580 | if (!empty($subject->publicKey)) { |
||
2581 | $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey; |
||
2582 | } |
||
2583 | $this->removeExtension('id-ce-authorityKeyIdentifier'); |
||
2584 | if (isset($subject->domains)) { |
||
2585 | $this->removeExtension('id-ce-subjectAltName'); |
||
2586 | } |
||
2587 | } elseif (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertList'])) { |
||
2588 | return false; |
||
2589 | } else { |
||
2590 | if (!isset($subject->publicKey)) { |
||
2591 | return false; |
||
2592 | } |
||
2593 | |||
2594 | $startDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); |
||
2595 | $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); |
||
2596 | |||
2597 | $endDate = new \DateTimeImmutable('+1 year', new \DateTimeZone(@date_default_timezone_get())); |
||
2598 | $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); |
||
2599 | |||
2600 | /* "The serial number MUST be a positive integer" |
||
2601 | "Conforming CAs MUST NOT use serialNumber values longer than 20 octets." |
||
2602 | -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2 |
||
2603 | |||
2604 | for the integer to be positive the leading bit needs to be 0 hence the |
||
2605 | application of a bitmap |
||
2606 | */ |
||
2607 | $serialNumber = !empty($this->serialNumber) ? |
||
2608 | $this->serialNumber : |
||
2609 | new BigInteger(Random::string(20) & ("\x7F" . str_repeat("\xFF", 19)), 256); |
||
2610 | |||
2611 | $this->currentCert = [ |
||
2612 | 'tbsCertificate' => |
||
2613 | [ |
||
2614 | 'version' => 'v3', |
||
2615 | 'serialNumber' => $serialNumber, // $this->setSerialNumber() |
||
2616 | 'signature' => $signatureAlgorithm, |
||
2617 | 'issuer' => false, // this is going to be overwritten later |
||
2618 | 'validity' => [ |
||
2619 | 'notBefore' => $this->timeField($startDate), // $this->setStartDate() |
||
2620 | 'notAfter' => $this->timeField($endDate) // $this->setEndDate() |
||
2621 | ], |
||
2622 | 'subject' => $subject->dn, |
||
2623 | 'subjectPublicKeyInfo' => $subjectPublicKey |
||
2624 | ], |
||
2625 | 'signatureAlgorithm' => $signatureAlgorithm, |
||
2626 | 'signature' => false // this is going to be overwritten later |
||
2627 | ]; |
||
2628 | |||
2629 | // Copy extensions from CSR. |
||
2630 | $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0); |
||
2631 | |||
2632 | if (!empty($csrexts)) { |
||
2633 | $this->currentCert['tbsCertificate']['extensions'] = $csrexts; |
||
2634 | } |
||
2635 | } |
||
2636 | |||
2637 | $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn; |
||
2638 | |||
2639 | if (isset($issuer->currentKeyIdentifier)) { |
||
2640 | $this->setExtension('id-ce-authorityKeyIdentifier', [ |
||
2641 | //'authorityCertIssuer' => array( |
||
2642 | // array( |
||
2643 | // 'directoryName' => $issuer->dn |
||
2644 | // ) |
||
2645 | //), |
||
2646 | 'keyIdentifier' => $issuer->currentKeyIdentifier |
||
2647 | ]); |
||
2648 | //$extensions = &$this->currentCert['tbsCertificate']['extensions']; |
||
2649 | //if (isset($issuer->serialNumber)) { |
||
2650 | // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; |
||
2651 | //} |
||
2652 | //unset($extensions); |
||
2653 | } |
||
2654 | |||
2655 | if (isset($subject->currentKeyIdentifier)) { |
||
2656 | $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier); |
||
2657 | } |
||
2658 | |||
2659 | $altName = []; |
||
2660 | |||
2661 | if (isset($subject->domains) && count($subject->domains)) { |
||
2662 | $altName = array_map(['\phpseclib3\File\X509', 'dnsName'], $subject->domains); |
||
2663 | } |
||
2664 | |||
2665 | if (isset($subject->ipAddresses) && count($subject->ipAddresses)) { |
||
2666 | // should an IP address appear as the CN if no domain name is specified? idk |
||
2667 | //$ips = count($subject->domains) ? $subject->ipAddresses : array_slice($subject->ipAddresses, 1); |
||
2668 | $ipAddresses = []; |
||
2669 | foreach ($subject->ipAddresses as $ipAddress) { |
||
2670 | $encoded = $subject->ipAddress($ipAddress); |
||
2671 | if ($encoded !== false) { |
||
2672 | $ipAddresses[] = $encoded; |
||
2673 | } |
||
2674 | } |
||
2675 | if (count($ipAddresses)) { |
||
2676 | $altName = array_merge($altName, $ipAddresses); |
||
2677 | } |
||
2678 | } |
||
2679 | |||
2680 | if (!empty($altName)) { |
||
2681 | $this->setExtension('id-ce-subjectAltName', $altName); |
||
2682 | } |
||
2683 | |||
2684 | if ($this->caFlag) { |
||
2685 | $keyUsage = $this->getExtension('id-ce-keyUsage'); |
||
2686 | if (!$keyUsage) { |
||
2687 | $keyUsage = []; |
||
2688 | } |
||
2689 | |||
2690 | $this->setExtension( |
||
2691 | 'id-ce-keyUsage', |
||
2692 | array_values(array_unique(array_merge($keyUsage, ['cRLSign', 'keyCertSign']))) |
||
2693 | ); |
||
2694 | |||
2695 | $basicConstraints = $this->getExtension('id-ce-basicConstraints'); |
||
2696 | if (!$basicConstraints) { |
||
2697 | $basicConstraints = []; |
||
2698 | } |
||
2699 | |||
2700 | $this->setExtension( |
||
2701 | 'id-ce-basicConstraints', |
||
2702 | array_merge(['cA' => true], $basicConstraints), |
||
2703 | true |
||
2704 | ); |
||
2705 | |||
2706 | if (!isset($subject->currentKeyIdentifier)) { |
||
2707 | $this->setExtension('id-ce-subjectKeyIdentifier', $this->computeKeyIdentifier($this->currentCert), false, false); |
||
2708 | } |
||
2709 | } |
||
2710 | |||
2711 | // resync $this->signatureSubject |
||
2712 | // save $tbsCertificate in case there are any \phpseclib3\File\ASN1\Element objects in it |
||
2713 | $tbsCertificate = $this->currentCert['tbsCertificate']; |
||
2714 | $this->loadX509($this->saveX509($this->currentCert)); |
||
2715 | |||
2716 | $result = $this->currentCert; |
||
2717 | $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject); |
||
2718 | $result['tbsCertificate'] = $tbsCertificate; |
||
2719 | |||
2720 | $this->currentCert = $currentCert; |
||
2721 | $this->signatureSubject = $signatureSubject; |
||
2722 | |||
2723 | return $result; |
||
2724 | } |
||
2725 | |||
2726 | /** |
||
2727 | * Sign a CSR |
||
2728 | * |
||
2729 | * @return mixed |
||
2730 | */ |
||
2731 | public function signCSR() |
||
2732 | { |
||
2733 | if (!is_object($this->privateKey) || empty($this->dn)) { |
||
2734 | return false; |
||
2735 | } |
||
2736 | |||
2737 | $origPublicKey = $this->publicKey; |
||
2738 | $this->publicKey = $this->privateKey->getPublicKey(); |
||
2739 | $publicKey = $this->formatSubjectPublicKey(); |
||
2740 | $this->publicKey = $origPublicKey; |
||
2741 | |||
2742 | $currentCert = isset($this->currentCert) ? $this->currentCert : null; |
||
2743 | $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; |
||
2744 | $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey); |
||
2745 | |||
2746 | if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['certificationRequestInfo'])) { |
||
1042 | daniel-mar | 2747 | $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; |
827 | daniel-mar | 2748 | if (!empty($this->dn)) { |
2749 | $this->currentCert['certificationRequestInfo']['subject'] = $this->dn; |
||
2750 | } |
||
2751 | $this->currentCert['certificationRequestInfo']['subjectPKInfo'] = $publicKey; |
||
2752 | } else { |
||
2753 | $this->currentCert = [ |
||
2754 | 'certificationRequestInfo' => |
||
2755 | [ |
||
2756 | 'version' => 'v1', |
||
2757 | 'subject' => $this->dn, |
||
2758 | 'subjectPKInfo' => $publicKey |
||
2759 | ], |
||
1042 | daniel-mar | 2760 | 'signatureAlgorithm' => $signatureAlgorithm, |
827 | daniel-mar | 2761 | 'signature' => false // this is going to be overwritten later |
2762 | ]; |
||
2763 | } |
||
2764 | |||
2765 | // resync $this->signatureSubject |
||
2766 | // save $certificationRequestInfo in case there are any \phpseclib3\File\ASN1\Element objects in it |
||
2767 | $certificationRequestInfo = $this->currentCert['certificationRequestInfo']; |
||
2768 | $this->loadCSR($this->saveCSR($this->currentCert)); |
||
2769 | |||
2770 | $result = $this->currentCert; |
||
2771 | $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject); |
||
2772 | $result['certificationRequestInfo'] = $certificationRequestInfo; |
||
2773 | |||
2774 | $this->currentCert = $currentCert; |
||
2775 | $this->signatureSubject = $signatureSubject; |
||
2776 | |||
2777 | return $result; |
||
2778 | } |
||
2779 | |||
2780 | /** |
||
2781 | * Sign a SPKAC |
||
2782 | * |
||
2783 | * @return mixed |
||
2784 | */ |
||
2785 | public function signSPKAC() |
||
2786 | { |
||
2787 | if (!is_object($this->privateKey)) { |
||
2788 | return false; |
||
2789 | } |
||
2790 | |||
2791 | $origPublicKey = $this->publicKey; |
||
2792 | $this->publicKey = $this->privateKey->getPublicKey(); |
||
2793 | $publicKey = $this->formatSubjectPublicKey(); |
||
2794 | $this->publicKey = $origPublicKey; |
||
2795 | |||
2796 | $currentCert = isset($this->currentCert) ? $this->currentCert : null; |
||
2797 | $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; |
||
2798 | $signatureAlgorithm = self::identifySignatureAlgorithm($this->privateKey); |
||
2799 | |||
2800 | // re-signing a SPKAC seems silly but since everything else supports re-signing why not? |
||
2801 | if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['publicKeyAndChallenge'])) { |
||
1042 | daniel-mar | 2802 | $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; |
827 | daniel-mar | 2803 | $this->currentCert['publicKeyAndChallenge']['spki'] = $publicKey; |
2804 | if (!empty($this->challenge)) { |
||
2805 | // the bitwise AND ensures that the output is a valid IA5String |
||
2806 | $this->currentCert['publicKeyAndChallenge']['challenge'] = $this->challenge & str_repeat("\x7F", strlen($this->challenge)); |
||
2807 | } |
||
2808 | } else { |
||
2809 | $this->currentCert = [ |
||
2810 | 'publicKeyAndChallenge' => |
||
2811 | [ |
||
2812 | 'spki' => $publicKey, |
||
2813 | // quoting <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen>, |
||
2814 | // "A challenge string that is submitted along with the public key. Defaults to an empty string if not specified." |
||
2815 | // both Firefox and OpenSSL ("openssl spkac -key private.key") behave this way |
||
2816 | // we could alternatively do this instead if we ignored the specs: |
||
2817 | // Random::string(8) & str_repeat("\x7F", 8) |
||
2818 | 'challenge' => !empty($this->challenge) ? $this->challenge : '' |
||
2819 | ], |
||
1042 | daniel-mar | 2820 | 'signatureAlgorithm' => $signatureAlgorithm, |
827 | daniel-mar | 2821 | 'signature' => false // this is going to be overwritten later |
2822 | ]; |
||
2823 | } |
||
2824 | |||
2825 | // resync $this->signatureSubject |
||
2826 | // save $publicKeyAndChallenge in case there are any \phpseclib3\File\ASN1\Element objects in it |
||
2827 | $publicKeyAndChallenge = $this->currentCert['publicKeyAndChallenge']; |
||
2828 | $this->loadSPKAC($this->saveSPKAC($this->currentCert)); |
||
2829 | |||
2830 | $result = $this->currentCert; |
||
2831 | $this->currentCert['signature'] = $result['signature'] = "\0" . $this->privateKey->sign($this->signatureSubject); |
||
2832 | $result['publicKeyAndChallenge'] = $publicKeyAndChallenge; |
||
2833 | |||
2834 | $this->currentCert = $currentCert; |
||
2835 | $this->signatureSubject = $signatureSubject; |
||
2836 | |||
2837 | return $result; |
||
2838 | } |
||
2839 | |||
2840 | /** |
||
2841 | * Sign a CRL |
||
2842 | * |
||
2843 | * $issuer's private key needs to be loaded. |
||
2844 | * |
||
2845 | * @return mixed |
||
2846 | */ |
||
1042 | daniel-mar | 2847 | public function signCRL(X509 $issuer, X509 $crl) |
827 | daniel-mar | 2848 | { |
2849 | if (!is_object($issuer->privateKey) || empty($issuer->dn)) { |
||
2850 | return false; |
||
2851 | } |
||
2852 | |||
2853 | $currentCert = isset($this->currentCert) ? $this->currentCert : null; |
||
2854 | $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; |
||
2855 | $signatureAlgorithm = self::identifySignatureAlgorithm($issuer->privateKey); |
||
2856 | |||
2857 | $thisUpdate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); |
||
2858 | $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O'); |
||
2859 | |||
2860 | if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) { |
||
2861 | $this->currentCert = $crl->currentCert; |
||
1042 | daniel-mar | 2862 | $this->currentCert['tbsCertList']['signature'] = $signatureAlgorithm; |
2863 | $this->currentCert['signatureAlgorithm'] = $signatureAlgorithm; |
||
827 | daniel-mar | 2864 | } else { |
2865 | $this->currentCert = [ |
||
2866 | 'tbsCertList' => |
||
2867 | [ |
||
2868 | 'version' => 'v2', |
||
1042 | daniel-mar | 2869 | 'signature' => $signatureAlgorithm, |
827 | daniel-mar | 2870 | 'issuer' => false, // this is going to be overwritten later |
2871 | 'thisUpdate' => $this->timeField($thisUpdate) // $this->setStartDate() |
||
2872 | ], |
||
1042 | daniel-mar | 2873 | 'signatureAlgorithm' => $signatureAlgorithm, |
827 | daniel-mar | 2874 | 'signature' => false // this is going to be overwritten later |
2875 | ]; |
||
2876 | } |
||
2877 | |||
2878 | $tbsCertList = &$this->currentCert['tbsCertList']; |
||
2879 | $tbsCertList['issuer'] = $issuer->dn; |
||
2880 | $tbsCertList['thisUpdate'] = $this->timeField($thisUpdate); |
||
2881 | |||
2882 | if (!empty($this->endDate)) { |
||
2883 | $tbsCertList['nextUpdate'] = $this->timeField($this->endDate); // $this->setEndDate() |
||
2884 | } else { |
||
2885 | unset($tbsCertList['nextUpdate']); |
||
2886 | } |
||
2887 | |||
2888 | if (!empty($this->serialNumber)) { |
||
2889 | $crlNumber = $this->serialNumber; |
||
2890 | } else { |
||
2891 | $crlNumber = $this->getExtension('id-ce-cRLNumber'); |
||
2892 | // "The CRL number is a non-critical CRL extension that conveys a |
||
2893 | // monotonically increasing sequence number for a given CRL scope and |
||
2894 | // CRL issuer. This extension allows users to easily determine when a |
||
2895 | // particular CRL supersedes another CRL." |
||
2896 | // -- https://tools.ietf.org/html/rfc5280#section-5.2.3 |
||
2897 | $crlNumber = $crlNumber !== false ? $crlNumber->add(new BigInteger(1)) : null; |
||
2898 | } |
||
2899 | |||
2900 | $this->removeExtension('id-ce-authorityKeyIdentifier'); |
||
2901 | $this->removeExtension('id-ce-issuerAltName'); |
||
2902 | |||
2903 | // Be sure version >= v2 if some extension found. |
||
2904 | $version = isset($tbsCertList['version']) ? $tbsCertList['version'] : 0; |
||
2905 | if (!$version) { |
||
2906 | if (!empty($tbsCertList['crlExtensions'])) { |
||
2907 | $version = 1; // v2. |
||
2908 | } elseif (!empty($tbsCertList['revokedCertificates'])) { |
||
2909 | foreach ($tbsCertList['revokedCertificates'] as $cert) { |
||
2910 | if (!empty($cert['crlEntryExtensions'])) { |
||
2911 | $version = 1; // v2. |
||
2912 | } |
||
2913 | } |
||
2914 | } |
||
2915 | |||
2916 | if ($version) { |
||
2917 | $tbsCertList['version'] = $version; |
||
2918 | } |
||
2919 | } |
||
2920 | |||
2921 | // Store additional extensions. |
||
2922 | if (!empty($tbsCertList['version'])) { // At least v2. |
||
2923 | if (!empty($crlNumber)) { |
||
2924 | $this->setExtension('id-ce-cRLNumber', $crlNumber); |
||
2925 | } |
||
2926 | |||
2927 | if (isset($issuer->currentKeyIdentifier)) { |
||
2928 | $this->setExtension('id-ce-authorityKeyIdentifier', [ |
||
2929 | //'authorityCertIssuer' => array( |
||
2930 | // ] |
||
2931 | // 'directoryName' => $issuer->dn |
||
2932 | // ] |
||
2933 | //), |
||
2934 | 'keyIdentifier' => $issuer->currentKeyIdentifier |
||
2935 | ]); |
||
2936 | //$extensions = &$tbsCertList['crlExtensions']; |
||
2937 | //if (isset($issuer->serialNumber)) { |
||
2938 | // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; |
||
2939 | //} |
||
2940 | //unset($extensions); |
||
2941 | } |
||
2942 | |||
2943 | $issuerAltName = $this->getExtension('id-ce-subjectAltName', $issuer->currentCert); |
||
2944 | |||
2945 | if ($issuerAltName !== false) { |
||
2946 | $this->setExtension('id-ce-issuerAltName', $issuerAltName); |
||
2947 | } |
||
2948 | } |
||
2949 | |||
2950 | if (empty($tbsCertList['revokedCertificates'])) { |
||
2951 | unset($tbsCertList['revokedCertificates']); |
||
2952 | } |
||
2953 | |||
2954 | unset($tbsCertList); |
||
2955 | |||
2956 | // resync $this->signatureSubject |
||
2957 | // save $tbsCertList in case there are any \phpseclib3\File\ASN1\Element objects in it |
||
2958 | $tbsCertList = $this->currentCert['tbsCertList']; |
||
2959 | $this->loadCRL($this->saveCRL($this->currentCert)); |
||
2960 | |||
2961 | $result = $this->currentCert; |
||
2962 | $this->currentCert['signature'] = $result['signature'] = "\0" . $issuer->privateKey->sign($this->signatureSubject); |
||
2963 | $result['tbsCertList'] = $tbsCertList; |
||
2964 | |||
2965 | $this->currentCert = $currentCert; |
||
2966 | $this->signatureSubject = $signatureSubject; |
||
2967 | |||
2968 | return $result; |
||
2969 | } |
||
2970 | |||
2971 | /** |
||
2972 | * Identify signature algorithm from key settings |
||
2973 | * |
||
2974 | * @param PrivateKey $key |
||
2975 | * @throws \phpseclib3\Exception\UnsupportedAlgorithmException if the algorithm is unsupported |
||
1042 | daniel-mar | 2976 | * @return array |
827 | daniel-mar | 2977 | */ |
2978 | private static function identifySignatureAlgorithm(PrivateKey $key) |
||
2979 | { |
||
2980 | if ($key instanceof RSA) { |
||
2981 | if ($key->getPadding() & RSA::SIGNATURE_PSS) { |
||
1042 | daniel-mar | 2982 | $r = PSS::load($key->withPassword()->toString('PSS')); |
2983 | return [ |
||
2984 | 'algorithm' => 'id-RSASSA-PSS', |
||
2985 | 'parameters' => PSS::savePSSParams($r) |
||
2986 | ]; |
||
827 | daniel-mar | 2987 | } |
2988 | switch ($key->getHash()) { |
||
2989 | case 'md2': |
||
2990 | case 'md5': |
||
2991 | case 'sha1': |
||
2992 | case 'sha224': |
||
2993 | case 'sha256': |
||
2994 | case 'sha384': |
||
2995 | case 'sha512': |
||
1042 | daniel-mar | 2996 | return ['algorithm' => $key->getHash() . 'WithRSAEncryption']; |
827 | daniel-mar | 2997 | } |
2998 | throw new UnsupportedAlgorithmException('The only supported hash algorithms for RSA are: md2, md5, sha1, sha224, sha256, sha384, sha512'); |
||
2999 | } |
||
3000 | |||
3001 | if ($key instanceof DSA) { |
||
3002 | switch ($key->getHash()) { |
||
3003 | case 'sha1': |
||
3004 | case 'sha224': |
||
3005 | case 'sha256': |
||
1042 | daniel-mar | 3006 | return ['algorithm' => 'id-dsa-with-' . $key->getHash()]; |
827 | daniel-mar | 3007 | } |
3008 | throw new UnsupportedAlgorithmException('The only supported hash algorithms for DSA are: sha1, sha224, sha256'); |
||
3009 | } |
||
3010 | |||
3011 | if ($key instanceof EC) { |
||
3012 | switch ($key->getCurve()) { |
||
3013 | case 'Ed25519': |
||
3014 | case 'Ed448': |
||
1042 | daniel-mar | 3015 | return ['algorithm' => 'id-' . $key->getCurve()]; |
827 | daniel-mar | 3016 | } |
3017 | switch ($key->getHash()) { |
||
3018 | case 'sha1': |
||
3019 | case 'sha224': |
||
3020 | case 'sha256': |
||
3021 | case 'sha384': |
||
3022 | case 'sha512': |
||
1042 | daniel-mar | 3023 | return ['algorithm' => 'ecdsa-with-' . strtoupper($key->getHash())]; |
827 | daniel-mar | 3024 | } |
3025 | throw new UnsupportedAlgorithmException('The only supported hash algorithms for EC are: sha1, sha224, sha256, sha384, sha512'); |
||
3026 | } |
||
3027 | |||
3028 | throw new UnsupportedAlgorithmException('The only supported public key classes are: RSA, DSA, EC'); |
||
3029 | } |
||
3030 | |||
3031 | /** |
||
3032 | * Set certificate start date |
||
3033 | * |
||
3034 | * @param \DateTimeInterface|string $date |
||
3035 | */ |
||
3036 | public function setStartDate($date) |
||
3037 | { |
||
3038 | if (!is_object($date) || !($date instanceof \DateTimeInterface)) { |
||
3039 | $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); |
||
3040 | } |
||
3041 | |||
3042 | $this->startDate = $date->format('D, d M Y H:i:s O'); |
||
3043 | } |
||
3044 | |||
3045 | /** |
||
3046 | * Set certificate end date |
||
3047 | * |
||
3048 | * @param \DateTimeInterface|string $date |
||
3049 | */ |
||
3050 | public function setEndDate($date) |
||
3051 | { |
||
3052 | /* |
||
3053 | To indicate that a certificate has no well-defined expiration date, |
||
3054 | the notAfter SHOULD be assigned the GeneralizedTime value of |
||
3055 | 99991231235959Z. |
||
3056 | |||
3057 | -- http://tools.ietf.org/html/rfc5280#section-4.1.2.5 |
||
3058 | */ |
||
3059 | if (is_string($date) && strtolower($date) === 'lifetime') { |
||
3060 | $temp = '99991231235959Z'; |
||
3061 | $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . ASN1::encodeLength(strlen($temp)) . $temp; |
||
3062 | $this->endDate = new Element($temp); |
||
3063 | } else { |
||
3064 | if (!is_object($date) || !($date instanceof \DateTimeInterface)) { |
||
3065 | $date = new \DateTimeImmutable($date, new \DateTimeZone(@date_default_timezone_get())); |
||
3066 | } |
||
3067 | |||
3068 | $this->endDate = $date->format('D, d M Y H:i:s O'); |
||
3069 | } |
||
3070 | } |
||
3071 | |||
3072 | /** |
||
3073 | * Set Serial Number |
||
3074 | * |
||
3075 | * @param string $serial |
||
3076 | * @param int $base optional |
||
3077 | */ |
||
3078 | public function setSerialNumber($serial, $base = -256) |
||
3079 | { |
||
3080 | $this->serialNumber = new BigInteger($serial, $base); |
||
3081 | } |
||
3082 | |||
3083 | /** |
||
3084 | * Turns the certificate into a certificate authority |
||
3085 | * |
||
3086 | */ |
||
3087 | public function makeCA() |
||
3088 | { |
||
3089 | $this->caFlag = true; |
||
3090 | } |
||
3091 | |||
3092 | /** |
||
3093 | * Check for validity of subarray |
||
3094 | * |
||
3095 | * This is intended for use in conjunction with _subArrayUnchecked(), |
||
3096 | * implementing the checks included in _subArray() but without copying |
||
3097 | * a potentially large array by passing its reference by-value to is_array(). |
||
3098 | * |
||
3099 | * @param array $root |
||
3100 | * @param string $path |
||
3101 | * @return boolean |
||
3102 | */ |
||
1042 | daniel-mar | 3103 | private function isSubArrayValid(array $root, $path) |
827 | daniel-mar | 3104 | { |
3105 | if (!is_array($root)) { |
||
3106 | return false; |
||
3107 | } |
||
3108 | |||
3109 | foreach (explode('/', $path) as $i) { |
||
3110 | if (!is_array($root)) { |
||
3111 | return false; |
||
3112 | } |
||
3113 | |||
3114 | if (!isset($root[$i])) { |
||
3115 | return true; |
||
3116 | } |
||
3117 | |||
3118 | $root = $root[$i]; |
||
3119 | } |
||
3120 | |||
3121 | return true; |
||
3122 | } |
||
3123 | |||
3124 | /** |
||
3125 | * Get a reference to a subarray |
||
3126 | * |
||
3127 | * This variant of _subArray() does no is_array() checking, |
||
3128 | * so $root should be checked with _isSubArrayValid() first. |
||
3129 | * |
||
3130 | * This is here for performance reasons: |
||
3131 | * Passing a reference (i.e. $root) by-value (i.e. to is_array()) |
||
3132 | * creates a copy. If $root is an especially large array, this is expensive. |
||
3133 | * |
||
3134 | * @param array $root |
||
3135 | * @param string $path absolute path with / as component separator |
||
3136 | * @param bool $create optional |
||
3137 | * @return array|false |
||
3138 | */ |
||
1042 | daniel-mar | 3139 | private function &subArrayUnchecked(array &$root, $path, $create = false) |
827 | daniel-mar | 3140 | { |
3141 | $false = false; |
||
3142 | |||
3143 | foreach (explode('/', $path) as $i) { |
||
3144 | if (!isset($root[$i])) { |
||
3145 | if (!$create) { |
||
3146 | return $false; |
||
3147 | } |
||
3148 | |||
3149 | $root[$i] = []; |
||
3150 | } |
||
3151 | |||
3152 | $root = &$root[$i]; |
||
3153 | } |
||
3154 | |||
3155 | return $root; |
||
3156 | } |
||
3157 | |||
3158 | /** |
||
3159 | * Get a reference to a subarray |
||
3160 | * |
||
3161 | * @param array $root |
||
3162 | * @param string $path absolute path with / as component separator |
||
3163 | * @param bool $create optional |
||
3164 | * @return array|false |
||
3165 | */ |
||
1042 | daniel-mar | 3166 | private function &subArray(array &$root = null, $path, $create = false) |
827 | daniel-mar | 3167 | { |
3168 | $false = false; |
||
3169 | |||
3170 | if (!is_array($root)) { |
||
3171 | return $false; |
||
3172 | } |
||
3173 | |||
3174 | foreach (explode('/', $path) as $i) { |
||
3175 | if (!is_array($root)) { |
||
3176 | return $false; |
||
3177 | } |
||
3178 | |||
3179 | if (!isset($root[$i])) { |
||
3180 | if (!$create) { |
||
3181 | return $false; |
||
3182 | } |
||
3183 | |||
3184 | $root[$i] = []; |
||
3185 | } |
||
3186 | |||
3187 | $root = &$root[$i]; |
||
3188 | } |
||
3189 | |||
3190 | return $root; |
||
3191 | } |
||
3192 | |||
3193 | /** |
||
3194 | * Get a reference to an extension subarray |
||
3195 | * |
||
3196 | * @param array $root |
||
3197 | * @param string $path optional absolute path with / as component separator |
||
3198 | * @param bool $create optional |
||
3199 | * @return array|false |
||
3200 | */ |
||
1042 | daniel-mar | 3201 | private function &extensions(array &$root = null, $path = null, $create = false) |
827 | daniel-mar | 3202 | { |
3203 | if (!isset($root)) { |
||
3204 | $root = $this->currentCert; |
||
3205 | } |
||
3206 | |||
3207 | switch (true) { |
||
3208 | case !empty($path): |
||
3209 | case !is_array($root): |
||
3210 | break; |
||
3211 | case isset($root['tbsCertificate']): |
||
3212 | $path = 'tbsCertificate/extensions'; |
||
3213 | break; |
||
3214 | case isset($root['tbsCertList']): |
||
3215 | $path = 'tbsCertList/crlExtensions'; |
||
3216 | break; |
||
3217 | case isset($root['certificationRequestInfo']): |
||
3218 | $pth = 'certificationRequestInfo/attributes'; |
||
3219 | $attributes = &$this->subArray($root, $pth, $create); |
||
3220 | |||
3221 | if (is_array($attributes)) { |
||
3222 | foreach ($attributes as $key => $value) { |
||
3223 | if ($value['type'] == 'pkcs-9-at-extensionRequest') { |
||
3224 | $path = "$pth/$key/value/0"; |
||
3225 | break 2; |
||
3226 | } |
||
3227 | } |
||
3228 | if ($create) { |
||
3229 | $key = count($attributes); |
||
3230 | $attributes[] = ['type' => 'pkcs-9-at-extensionRequest', 'value' => []]; |
||
3231 | $path = "$pth/$key/value/0"; |
||
3232 | } |
||
3233 | } |
||
3234 | break; |
||
3235 | } |
||
3236 | |||
3237 | $extensions = &$this->subArray($root, $path, $create); |
||
3238 | |||
3239 | if (!is_array($extensions)) { |
||
3240 | $false = false; |
||
3241 | return $false; |
||
3242 | } |
||
3243 | |||
3244 | return $extensions; |
||
3245 | } |
||
3246 | |||
3247 | /** |
||
3248 | * Remove an Extension |
||
3249 | * |
||
3250 | * @param string $id |
||
3251 | * @param string $path optional |
||
3252 | * @return bool |
||
3253 | */ |
||
3254 | private function removeExtensionHelper($id, $path = null) |
||
3255 | { |
||
3256 | $extensions = &$this->extensions($this->currentCert, $path); |
||
3257 | |||
3258 | if (!is_array($extensions)) { |
||
3259 | return false; |
||
3260 | } |
||
3261 | |||
3262 | $result = false; |
||
3263 | foreach ($extensions as $key => $value) { |
||
3264 | if ($value['extnId'] == $id) { |
||
3265 | unset($extensions[$key]); |
||
3266 | $result = true; |
||
3267 | } |
||
3268 | } |
||
3269 | |||
3270 | $extensions = array_values($extensions); |
||
3271 | // fix for https://bugs.php.net/75433 affecting PHP 7.2 |
||
3272 | if (!isset($extensions[0])) { |
||
3273 | $extensions = array_splice($extensions, 0, 0); |
||
3274 | } |
||
3275 | return $result; |
||
3276 | } |
||
3277 | |||
3278 | /** |
||
3279 | * Get an Extension |
||
3280 | * |
||
3281 | * Returns the extension if it exists and false if not |
||
3282 | * |
||
3283 | * @param string $id |
||
3284 | * @param array $cert optional |
||
3285 | * @param string $path optional |
||
3286 | * @return mixed |
||
3287 | */ |
||
1042 | daniel-mar | 3288 | private function getExtensionHelper($id, array $cert = null, $path = null) |
827 | daniel-mar | 3289 | { |
3290 | $extensions = $this->extensions($cert, $path); |
||
3291 | |||
3292 | if (!is_array($extensions)) { |
||
3293 | return false; |
||
3294 | } |
||
3295 | |||
3296 | foreach ($extensions as $key => $value) { |
||
3297 | if ($value['extnId'] == $id) { |
||
3298 | return $value['extnValue']; |
||
3299 | } |
||
3300 | } |
||
3301 | |||
3302 | return false; |
||
3303 | } |
||
3304 | |||
3305 | /** |
||
3306 | * Returns a list of all extensions in use |
||
3307 | * |
||
3308 | * @param array $cert optional |
||
3309 | * @param string $path optional |
||
3310 | * @return array |
||
3311 | */ |
||
1042 | daniel-mar | 3312 | private function getExtensionsHelper(array $cert = null, $path = null) |
827 | daniel-mar | 3313 | { |
3314 | $exts = $this->extensions($cert, $path); |
||
3315 | $extensions = []; |
||
3316 | |||
3317 | if (is_array($exts)) { |
||
3318 | foreach ($exts as $extension) { |
||
3319 | $extensions[] = $extension['extnId']; |
||
3320 | } |
||
3321 | } |
||
3322 | |||
3323 | return $extensions; |
||
3324 | } |
||
3325 | |||
3326 | /** |
||
3327 | * Set an Extension |
||
3328 | * |
||
3329 | * @param string $id |
||
3330 | * @param mixed $value |
||
3331 | * @param bool $critical optional |
||
3332 | * @param bool $replace optional |
||
3333 | * @param string $path optional |
||
3334 | * @return bool |
||
3335 | */ |
||
3336 | private function setExtensionHelper($id, $value, $critical = false, $replace = true, $path = null) |
||
3337 | { |
||
3338 | $extensions = &$this->extensions($this->currentCert, $path, true); |
||
3339 | |||
3340 | if (!is_array($extensions)) { |
||
3341 | return false; |
||
3342 | } |
||
3343 | |||
3344 | $newext = ['extnId' => $id, 'critical' => $critical, 'extnValue' => $value]; |
||
3345 | |||
3346 | foreach ($extensions as $key => $value) { |
||
3347 | if ($value['extnId'] == $id) { |
||
3348 | if (!$replace) { |
||
3349 | return false; |
||
3350 | } |
||
3351 | |||
3352 | $extensions[$key] = $newext; |
||
3353 | return true; |
||
3354 | } |
||
3355 | } |
||
3356 | |||
3357 | $extensions[] = $newext; |
||
3358 | return true; |
||
3359 | } |
||
3360 | |||
3361 | /** |
||
3362 | * Remove a certificate, CSR or CRL Extension |
||
3363 | * |
||
3364 | * @param string $id |
||
3365 | * @return bool |
||
3366 | */ |
||
3367 | public function removeExtension($id) |
||
3368 | { |
||
3369 | return $this->removeExtensionHelper($id); |
||
3370 | } |
||
3371 | |||
3372 | /** |
||
3373 | * Get a certificate, CSR or CRL Extension |
||
3374 | * |
||
3375 | * Returns the extension if it exists and false if not |
||
3376 | * |
||
3377 | * @param string $id |
||
3378 | * @param array $cert optional |
||
3379 | * @param string $path |
||
3380 | * @return mixed |
||
3381 | */ |
||
1042 | daniel-mar | 3382 | public function getExtension($id, array $cert = null, $path = null) |
827 | daniel-mar | 3383 | { |
3384 | return $this->getExtensionHelper($id, $cert, $path); |
||
3385 | } |
||
3386 | |||
3387 | /** |
||
3388 | * Returns a list of all extensions in use in certificate, CSR or CRL |
||
3389 | * |
||
3390 | * @param array $cert optional |
||
3391 | * @param string $path optional |
||
3392 | * @return array |
||
3393 | */ |
||
1042 | daniel-mar | 3394 | public function getExtensions(array $cert = null, $path = null) |
827 | daniel-mar | 3395 | { |
3396 | return $this->getExtensionsHelper($cert, $path); |
||
3397 | } |
||
3398 | |||
3399 | /** |
||
3400 | * Set a certificate, CSR or CRL Extension |
||
3401 | * |
||
3402 | * @param string $id |
||
3403 | * @param mixed $value |
||
3404 | * @param bool $critical optional |
||
3405 | * @param bool $replace optional |
||
3406 | * @return bool |
||
3407 | */ |
||
3408 | public function setExtension($id, $value, $critical = false, $replace = true) |
||
3409 | { |
||
3410 | return $this->setExtensionHelper($id, $value, $critical, $replace); |
||
3411 | } |
||
3412 | |||
3413 | /** |
||
3414 | * Remove a CSR attribute. |
||
3415 | * |
||
3416 | * @param string $id |
||
3417 | * @param int $disposition optional |
||
3418 | * @return bool |
||
3419 | */ |
||
3420 | public function removeAttribute($id, $disposition = self::ATTR_ALL) |
||
3421 | { |
||
3422 | $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes'); |
||
3423 | |||
3424 | if (!is_array($attributes)) { |
||
3425 | return false; |
||
3426 | } |
||
3427 | |||
3428 | $result = false; |
||
3429 | foreach ($attributes as $key => $attribute) { |
||
3430 | if ($attribute['type'] == $id) { |
||
3431 | $n = count($attribute['value']); |
||
3432 | switch (true) { |
||
3433 | case $disposition == self::ATTR_APPEND: |
||
3434 | case $disposition == self::ATTR_REPLACE: |
||
3435 | return false; |
||
3436 | case $disposition >= $n: |
||
3437 | $disposition -= $n; |
||
3438 | break; |
||
3439 | case $disposition == self::ATTR_ALL: |
||
3440 | case $n == 1: |
||
3441 | unset($attributes[$key]); |
||
3442 | $result = true; |
||
3443 | break; |
||
3444 | default: |
||
3445 | unset($attributes[$key]['value'][$disposition]); |
||
3446 | $attributes[$key]['value'] = array_values($attributes[$key]['value']); |
||
3447 | $result = true; |
||
3448 | break; |
||
3449 | } |
||
3450 | if ($result && $disposition != self::ATTR_ALL) { |
||
3451 | break; |
||
3452 | } |
||
3453 | } |
||
3454 | } |
||
3455 | |||
3456 | $attributes = array_values($attributes); |
||
3457 | return $result; |
||
3458 | } |
||
3459 | |||
3460 | /** |
||
3461 | * Get a CSR attribute |
||
3462 | * |
||
3463 | * Returns the attribute if it exists and false if not |
||
3464 | * |
||
3465 | * @param string $id |
||
3466 | * @param int $disposition optional |
||
3467 | * @param array $csr optional |
||
3468 | * @return mixed |
||
3469 | */ |
||
1042 | daniel-mar | 3470 | public function getAttribute($id, $disposition = self::ATTR_ALL, array $csr = null) |
827 | daniel-mar | 3471 | { |
3472 | if (empty($csr)) { |
||
3473 | $csr = $this->currentCert; |
||
3474 | } |
||
3475 | |||
3476 | $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes'); |
||
3477 | |||
3478 | if (!is_array($attributes)) { |
||
3479 | return false; |
||
3480 | } |
||
3481 | |||
3482 | foreach ($attributes as $key => $attribute) { |
||
3483 | if ($attribute['type'] == $id) { |
||
3484 | $n = count($attribute['value']); |
||
3485 | switch (true) { |
||
3486 | case $disposition == self::ATTR_APPEND: |
||
3487 | case $disposition == self::ATTR_REPLACE: |
||
3488 | return false; |
||
3489 | case $disposition == self::ATTR_ALL: |
||
3490 | return $attribute['value']; |
||
3491 | case $disposition >= $n: |
||
3492 | $disposition -= $n; |
||
3493 | break; |
||
3494 | default: |
||
3495 | return $attribute['value'][$disposition]; |
||
3496 | } |
||
3497 | } |
||
3498 | } |
||
3499 | |||
3500 | return false; |
||
3501 | } |
||
3502 | |||
3503 | /** |
||
3504 | * Returns a list of all CSR attributes in use |
||
3505 | * |
||
3506 | * @param array $csr optional |
||
3507 | * @return array |
||
3508 | */ |
||
1042 | daniel-mar | 3509 | public function getAttributes(array $csr = null) |
827 | daniel-mar | 3510 | { |
3511 | if (empty($csr)) { |
||
3512 | $csr = $this->currentCert; |
||
3513 | } |
||
3514 | |||
3515 | $attributes = $this->subArray($csr, 'certificationRequestInfo/attributes'); |
||
3516 | $attrs = []; |
||
3517 | |||
3518 | if (is_array($attributes)) { |
||
3519 | foreach ($attributes as $attribute) { |
||
3520 | $attrs[] = $attribute['type']; |
||
3521 | } |
||
3522 | } |
||
3523 | |||
3524 | return $attrs; |
||
3525 | } |
||
3526 | |||
3527 | /** |
||
3528 | * Set a CSR attribute |
||
3529 | * |
||
3530 | * @param string $id |
||
3531 | * @param mixed $value |
||
3532 | * @param int $disposition optional |
||
3533 | * @return bool |
||
3534 | */ |
||
3535 | public function setAttribute($id, $value, $disposition = self::ATTR_ALL) |
||
3536 | { |
||
3537 | $attributes = &$this->subArray($this->currentCert, 'certificationRequestInfo/attributes', true); |
||
3538 | |||
3539 | if (!is_array($attributes)) { |
||
3540 | return false; |
||
3541 | } |
||
3542 | |||
3543 | switch ($disposition) { |
||
3544 | case self::ATTR_REPLACE: |
||
3545 | $disposition = self::ATTR_APPEND; |
||
3546 | // fall-through |
||
3547 | case self::ATTR_ALL: |
||
3548 | $this->removeAttribute($id); |
||
3549 | break; |
||
3550 | } |
||
3551 | |||
3552 | foreach ($attributes as $key => $attribute) { |
||
3553 | if ($attribute['type'] == $id) { |
||
3554 | $n = count($attribute['value']); |
||
3555 | switch (true) { |
||
3556 | case $disposition == self::ATTR_APPEND: |
||
3557 | $last = $key; |
||
3558 | break; |
||
3559 | case $disposition >= $n: |
||
3560 | $disposition -= $n; |
||
3561 | break; |
||
3562 | default: |
||
3563 | $attributes[$key]['value'][$disposition] = $value; |
||
3564 | return true; |
||
3565 | } |
||
3566 | } |
||
3567 | } |
||
3568 | |||
3569 | switch (true) { |
||
3570 | case $disposition >= 0: |
||
3571 | return false; |
||
3572 | case isset($last): |
||
3573 | $attributes[$last]['value'][] = $value; |
||
3574 | break; |
||
3575 | default: |
||
3576 | $attributes[] = ['type' => $id, 'value' => $disposition == self::ATTR_ALL ? $value : [$value]]; |
||
3577 | break; |
||
3578 | } |
||
3579 | |||
3580 | return true; |
||
3581 | } |
||
3582 | |||
3583 | /** |
||
3584 | * Sets the subject key identifier |
||
3585 | * |
||
3586 | * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions. |
||
3587 | * |
||
3588 | * @param string $value |
||
3589 | */ |
||
3590 | public function setKeyIdentifier($value) |
||
3591 | { |
||
3592 | if (empty($value)) { |
||
3593 | unset($this->currentKeyIdentifier); |
||
3594 | } else { |
||
3595 | $this->currentKeyIdentifier = $value; |
||
3596 | } |
||
3597 | } |
||
3598 | |||
3599 | /** |
||
3600 | * Compute a public key identifier. |
||
3601 | * |
||
3602 | * Although key identifiers may be set to any unique value, this function |
||
3603 | * computes key identifiers from public key according to the two |
||
3604 | * recommended methods (4.2.1.2 RFC 3280). |
||
3605 | * Highly polymorphic: try to accept all possible forms of key: |
||
3606 | * - Key object |
||
3607 | * - \phpseclib3\File\X509 object with public or private key defined |
||
3608 | * - Certificate or CSR array |
||
3609 | * - \phpseclib3\File\ASN1\Element object |
||
3610 | * - PEM or DER string |
||
3611 | * |
||
3612 | * @param mixed $key optional |
||
3613 | * @param int $method optional |
||
3614 | * @return string binary key identifier |
||
3615 | */ |
||
3616 | public function computeKeyIdentifier($key = null, $method = 1) |
||
3617 | { |
||
3618 | if (is_null($key)) { |
||
3619 | $key = $this; |
||
3620 | } |
||
3621 | |||
3622 | switch (true) { |
||
3623 | case is_string($key): |
||
3624 | break; |
||
3625 | case is_array($key) && isset($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): |
||
3626 | return $this->computeKeyIdentifier($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $method); |
||
3627 | case is_array($key) && isset($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): |
||
3628 | return $this->computeKeyIdentifier($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $method); |
||
3629 | case !is_object($key): |
||
3630 | return false; |
||
3631 | case $key instanceof Element: |
||
3632 | // Assume the element is a bitstring-packed key. |
||
3633 | $decoded = ASN1::decodeBER($key->element); |
||
1042 | daniel-mar | 3634 | if (!$decoded) { |
827 | daniel-mar | 3635 | return false; |
3636 | } |
||
3637 | $raw = ASN1::asn1map($decoded[0], ['type' => ASN1::TYPE_BIT_STRING]); |
||
3638 | if (empty($raw)) { |
||
3639 | return false; |
||
3640 | } |
||
3641 | // If the key is private, compute identifier from its corresponding public key. |
||
3642 | $key = PublicKeyLoader::load($raw); |
||
3643 | if ($key instanceof PrivateKey) { // If private. |
||
3644 | return $this->computeKeyIdentifier($key, $method); |
||
3645 | } |
||
3646 | $key = $raw; // Is a public key. |
||
3647 | break; |
||
3648 | case $key instanceof X509: |
||
3649 | if (isset($key->publicKey)) { |
||
3650 | return $this->computeKeyIdentifier($key->publicKey, $method); |
||
3651 | } |
||
3652 | if (isset($key->privateKey)) { |
||
3653 | return $this->computeKeyIdentifier($key->privateKey, $method); |
||
3654 | } |
||
3655 | if (isset($key->currentCert['tbsCertificate']) || isset($key->currentCert['certificationRequestInfo'])) { |
||
3656 | return $this->computeKeyIdentifier($key->currentCert, $method); |
||
3657 | } |
||
3658 | return false; |
||
3659 | default: // Should be a key object (i.e.: \phpseclib3\Crypt\RSA). |
||
3660 | $key = $key->getPublicKey(); |
||
3661 | break; |
||
3662 | } |
||
3663 | |||
3664 | // If in PEM format, convert to binary. |
||
3665 | $key = ASN1::extractBER($key); |
||
3666 | |||
3667 | // Now we have the key string: compute its sha-1 sum. |
||
3668 | $hash = new Hash('sha1'); |
||
3669 | $hash = $hash->hash($key); |
||
3670 | |||
3671 | if ($method == 2) { |
||
3672 | $hash = substr($hash, -8); |
||
3673 | $hash[0] = chr((ord($hash[0]) & 0x0F) | 0x40); |
||
3674 | } |
||
3675 | |||
3676 | return $hash; |
||
3677 | } |
||
3678 | |||
3679 | /** |
||
3680 | * Format a public key as appropriate |
||
3681 | * |
||
3682 | * @return array|false |
||
3683 | */ |
||
3684 | private function formatSubjectPublicKey() |
||
3685 | { |
||
3686 | $format = $this->publicKey instanceof RSA && ($this->publicKey->getPadding() & RSA::SIGNATURE_PSS) ? |
||
3687 | 'PSS' : |
||
3688 | 'PKCS8'; |
||
3689 | |||
3690 | $publicKey = base64_decode(preg_replace('#-.+-|[\r\n]#', '', $this->publicKey->toString($format))); |
||
3691 | |||
3692 | $decoded = ASN1::decodeBER($publicKey); |
||
1042 | daniel-mar | 3693 | if (!$decoded) { |
3694 | return false; |
||
3695 | } |
||
827 | daniel-mar | 3696 | $mapped = ASN1::asn1map($decoded[0], Maps\SubjectPublicKeyInfo::MAP); |
3697 | if (!is_array($mapped)) { |
||
3698 | return false; |
||
3699 | } |
||
3700 | |||
3701 | $mapped['subjectPublicKey'] = $this->publicKey->toString($format); |
||
3702 | |||
3703 | return $mapped; |
||
3704 | } |
||
3705 | |||
3706 | /** |
||
3707 | * Set the domain name's which the cert is to be valid for |
||
3708 | * |
||
3709 | * @param mixed ...$domains |
||
3710 | * @return void |
||
3711 | */ |
||
3712 | public function setDomain(...$domains) |
||
3713 | { |
||
3714 | $this->domains = $domains; |
||
3715 | $this->removeDNProp('id-at-commonName'); |
||
3716 | $this->setDNProp('id-at-commonName', $this->domains[0]); |
||
3717 | } |
||
3718 | |||
3719 | /** |
||
3720 | * Set the IP Addresses's which the cert is to be valid for |
||
3721 | * |
||
3722 | * @param mixed[] ...$ipAddresses |
||
3723 | */ |
||
3724 | public function setIPAddress(...$ipAddresses) |
||
3725 | { |
||
3726 | $this->ipAddresses = $ipAddresses; |
||
3727 | /* |
||
3728 | if (!isset($this->domains)) { |
||
3729 | $this->removeDNProp('id-at-commonName'); |
||
3730 | $this->setDNProp('id-at-commonName', $this->ipAddresses[0]); |
||
3731 | } |
||
3732 | */ |
||
3733 | } |
||
3734 | |||
3735 | /** |
||
3736 | * Helper function to build domain array |
||
3737 | * |
||
3738 | * @param string $domain |
||
3739 | * @return array |
||
3740 | */ |
||
1042 | daniel-mar | 3741 | private static function dnsName($domain) |
827 | daniel-mar | 3742 | { |
3743 | return ['dNSName' => $domain]; |
||
3744 | } |
||
3745 | |||
3746 | /** |
||
3747 | * Helper function to build IP Address array |
||
3748 | * |
||
3749 | * (IPv6 is not currently supported) |
||
3750 | * |
||
3751 | * @param string $address |
||
3752 | * @return array |
||
3753 | */ |
||
3754 | private function iPAddress($address) |
||
3755 | { |
||
3756 | return ['iPAddress' => $address]; |
||
3757 | } |
||
3758 | |||
3759 | /** |
||
3760 | * Get the index of a revoked certificate. |
||
3761 | * |
||
3762 | * @param array $rclist |
||
3763 | * @param string $serial |
||
3764 | * @param bool $create optional |
||
3765 | * @return int|false |
||
3766 | */ |
||
1042 | daniel-mar | 3767 | private function revokedCertificate(array &$rclist, $serial, $create = false) |
827 | daniel-mar | 3768 | { |
3769 | $serial = new BigInteger($serial); |
||
3770 | |||
3771 | foreach ($rclist as $i => $rc) { |
||
3772 | if (!($serial->compare($rc['userCertificate']))) { |
||
3773 | return $i; |
||
3774 | } |
||
3775 | } |
||
3776 | |||
3777 | if (!$create) { |
||
3778 | return false; |
||
3779 | } |
||
3780 | |||
3781 | $i = count($rclist); |
||
3782 | $revocationDate = new \DateTimeImmutable('now', new \DateTimeZone(@date_default_timezone_get())); |
||
3783 | $rclist[] = ['userCertificate' => $serial, |
||
3784 | 'revocationDate' => $this->timeField($revocationDate->format('D, d M Y H:i:s O'))]; |
||
3785 | return $i; |
||
3786 | } |
||
3787 | |||
3788 | /** |
||
3789 | * Revoke a certificate. |
||
3790 | * |
||
3791 | * @param string $serial |
||
3792 | * @param string $date optional |
||
3793 | * @return bool |
||
3794 | */ |
||
3795 | public function revoke($serial, $date = null) |
||
3796 | { |
||
3797 | if (isset($this->currentCert['tbsCertList'])) { |
||
3798 | if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { |
||
3799 | if ($this->revokedCertificate($rclist, $serial) === false) { // If not yet revoked |
||
3800 | if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) { |
||
3801 | if (!empty($date)) { |
||
3802 | $rclist[$i]['revocationDate'] = $this->timeField($date); |
||
3803 | } |
||
3804 | |||
3805 | return true; |
||
3806 | } |
||
3807 | } |
||
3808 | } |
||
3809 | } |
||
3810 | |||
3811 | return false; |
||
3812 | } |
||
3813 | |||
3814 | /** |
||
3815 | * Unrevoke a certificate. |
||
3816 | * |
||
3817 | * @param string $serial |
||
3818 | * @return bool |
||
3819 | */ |
||
3820 | public function unrevoke($serial) |
||
3821 | { |
||
3822 | if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { |
||
3823 | if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { |
||
3824 | unset($rclist[$i]); |
||
3825 | $rclist = array_values($rclist); |
||
3826 | return true; |
||
3827 | } |
||
3828 | } |
||
3829 | |||
3830 | return false; |
||
3831 | } |
||
3832 | |||
3833 | /** |
||
3834 | * Get a revoked certificate. |
||
3835 | * |
||
3836 | * @param string $serial |
||
3837 | * @return mixed |
||
3838 | */ |
||
3839 | public function getRevoked($serial) |
||
3840 | { |
||
3841 | if (is_array($rclist = $this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { |
||
3842 | if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { |
||
3843 | return $rclist[$i]; |
||
3844 | } |
||
3845 | } |
||
3846 | |||
3847 | return false; |
||
3848 | } |
||
3849 | |||
3850 | /** |
||
3851 | * List revoked certificates |
||
3852 | * |
||
3853 | * @param array $crl optional |
||
3854 | * @return array|bool |
||
3855 | */ |
||
1042 | daniel-mar | 3856 | public function listRevoked(array $crl = null) |
827 | daniel-mar | 3857 | { |
3858 | if (!isset($crl)) { |
||
3859 | $crl = $this->currentCert; |
||
3860 | } |
||
3861 | |||
3862 | if (!isset($crl['tbsCertList'])) { |
||
3863 | return false; |
||
3864 | } |
||
3865 | |||
3866 | $result = []; |
||
3867 | |||
3868 | if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { |
||
3869 | foreach ($rclist as $rc) { |
||
3870 | $result[] = $rc['userCertificate']->toString(); |
||
3871 | } |
||
3872 | } |
||
3873 | |||
3874 | return $result; |
||
3875 | } |
||
3876 | |||
3877 | /** |
||
3878 | * Remove a Revoked Certificate Extension |
||
3879 | * |
||
3880 | * @param string $serial |
||
3881 | * @param string $id |
||
3882 | * @return bool |
||
3883 | */ |
||
3884 | public function removeRevokedCertificateExtension($serial, $id) |
||
3885 | { |
||
3886 | if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { |
||
3887 | if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { |
||
3888 | return $this->removeExtensionHelper($id, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); |
||
3889 | } |
||
3890 | } |
||
3891 | |||
3892 | return false; |
||
3893 | } |
||
3894 | |||
3895 | /** |
||
3896 | * Get a Revoked Certificate Extension |
||
3897 | * |
||
3898 | * Returns the extension if it exists and false if not |
||
3899 | * |
||
3900 | * @param string $serial |
||
3901 | * @param string $id |
||
3902 | * @param array $crl optional |
||
3903 | * @return mixed |
||
3904 | */ |
||
1042 | daniel-mar | 3905 | public function getRevokedCertificateExtension($serial, $id, array $crl = null) |
827 | daniel-mar | 3906 | { |
3907 | if (!isset($crl)) { |
||
3908 | $crl = $this->currentCert; |
||
3909 | } |
||
3910 | |||
3911 | if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { |
||
3912 | if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { |
||
3913 | return $this->getExtension($id, $crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); |
||
3914 | } |
||
3915 | } |
||
3916 | |||
3917 | return false; |
||
3918 | } |
||
3919 | |||
3920 | /** |
||
3921 | * Returns a list of all extensions in use for a given revoked certificate |
||
3922 | * |
||
3923 | * @param string $serial |
||
3924 | * @param array $crl optional |
||
3925 | * @return array|bool |
||
3926 | */ |
||
1042 | daniel-mar | 3927 | public function getRevokedCertificateExtensions($serial, array $crl = null) |
827 | daniel-mar | 3928 | { |
3929 | if (!isset($crl)) { |
||
3930 | $crl = $this->currentCert; |
||
3931 | } |
||
3932 | |||
3933 | if (is_array($rclist = $this->subArray($crl, 'tbsCertList/revokedCertificates'))) { |
||
3934 | if (($i = $this->revokedCertificate($rclist, $serial)) !== false) { |
||
3935 | return $this->getExtensions($crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); |
||
3936 | } |
||
3937 | } |
||
3938 | |||
3939 | return false; |
||
3940 | } |
||
3941 | |||
3942 | /** |
||
3943 | * Set a Revoked Certificate Extension |
||
3944 | * |
||
3945 | * @param string $serial |
||
3946 | * @param string $id |
||
3947 | * @param mixed $value |
||
3948 | * @param bool $critical optional |
||
3949 | * @param bool $replace optional |
||
3950 | * @return bool |
||
3951 | */ |
||
3952 | public function setRevokedCertificateExtension($serial, $id, $value, $critical = false, $replace = true) |
||
3953 | { |
||
3954 | if (isset($this->currentCert['tbsCertList'])) { |
||
3955 | if (is_array($rclist = &$this->subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { |
||
3956 | if (($i = $this->revokedCertificate($rclist, $serial, true)) !== false) { |
||
3957 | return $this->setExtensionHelper($id, $value, $critical, $replace, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); |
||
3958 | } |
||
3959 | } |
||
3960 | } |
||
3961 | |||
3962 | return false; |
||
3963 | } |
||
3964 | |||
3965 | /** |
||
3966 | * Register the mapping for a custom/unsupported extension. |
||
3967 | * |
||
3968 | * @param string $id |
||
3969 | * @param array $mapping |
||
3970 | */ |
||
3971 | public static function registerExtension($id, array $mapping) |
||
3972 | { |
||
3973 | if (isset(self::$extensions[$id]) && self::$extensions[$id] !== $mapping) { |
||
3974 | throw new \RuntimeException( |
||
3975 | 'Extension ' . $id . ' has already been defined with a different mapping.' |
||
3976 | ); |
||
3977 | } |
||
3978 | |||
3979 | self::$extensions[$id] = $mapping; |
||
3980 | } |
||
3981 | |||
3982 | /** |
||
3983 | * Register the mapping for a custom/unsupported extension. |
||
3984 | * |
||
3985 | * @param string $id |
||
3986 | * |
||
3987 | * @return array|null |
||
3988 | */ |
||
3989 | public static function getRegisteredExtension($id) |
||
3990 | { |
||
3991 | return isset(self::$extensions[$id]) ? self::$extensions[$id] : null; |
||
3992 | } |
||
3993 | |||
3994 | /** |
||
3995 | * Register the mapping for a custom/unsupported extension. |
||
3996 | * |
||
3997 | * @param string $id |
||
3998 | * @param mixed $value |
||
3999 | * @param bool $critical |
||
4000 | * @param bool $replace |
||
4001 | */ |
||
4002 | public function setExtensionValue($id, $value, $critical = false, $replace = false) |
||
4003 | { |
||
4004 | $this->extensionValues[$id] = compact('critical', 'replace', 'value'); |
||
4005 | } |
||
4006 | } |