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