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