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