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