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