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
25namespace phpseclib3\File;
26
27use phpseclib3\Common\Functions\Strings;
28use phpseclib3\Crypt\Common\PrivateKey;
29use phpseclib3\Crypt\Common\PublicKey;
30use phpseclib3\Crypt\DSA;
31use phpseclib3\Crypt\EC;
32use phpseclib3\Crypt\Hash;
33use phpseclib3\Crypt\PublicKeyLoader;
34use phpseclib3\Crypt\Random;
35use phpseclib3\Crypt\RSA;
36use phpseclib3\Crypt\RSA\Formats\Keys\PSS;
37use phpseclib3\Exception\UnsupportedAlgorithmException;
38use phpseclib3\File\ASN1\Element;
39use phpseclib3\File\ASN1\Maps;
40use phpseclib3\Math\BigInteger;
41
42/**
43 * Pure-PHP X.509 Parser
44 *
45 * @author  Jim Wigginton <terrafrost@php.net>
46 */
47class 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