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