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