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