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