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