[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/vendor/phpseclib/phpseclib/phpseclib/File/ -> X509.php (source)

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