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