1<?php 2 3/** 4 * RSA Public Key 5 * 6 * @author Jim Wigginton <terrafrost@php.net> 7 * @copyright 2015 Jim Wigginton 8 * @license http://www.opensource.org/licenses/mit-license.html MIT License 9 * @link http://phpseclib.sourceforge.net 10 */ 11 12namespace phpseclib3\Crypt\RSA; 13 14use phpseclib3\Common\Functions\Strings; 15use phpseclib3\Crypt\Common; 16use phpseclib3\Crypt\Hash; 17use phpseclib3\Crypt\Random; 18use phpseclib3\Crypt\RSA; 19use phpseclib3\Crypt\RSA\Formats\Keys\PSS; 20use phpseclib3\Exception\BadConfigurationException; 21use phpseclib3\Exception\UnsupportedAlgorithmException; 22use phpseclib3\Exception\UnsupportedFormatException; 23use phpseclib3\File\ASN1; 24use phpseclib3\File\ASN1\Maps\DigestInfo; 25use phpseclib3\Math\BigInteger; 26 27/** 28 * Raw RSA Key Handler 29 * 30 * @author Jim Wigginton <terrafrost@php.net> 31 */ 32final class PublicKey extends RSA implements Common\PublicKey 33{ 34 use Common\Traits\Fingerprint; 35 36 /** 37 * Exponentiate 38 * 39 * @param BigInteger $x 40 * @return BigInteger 41 */ 42 private function exponentiate(BigInteger $x) 43 { 44 return $x->modPow($this->exponent, $this->modulus); 45 } 46 47 /** 48 * RSAVP1 49 * 50 * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.2 RFC3447#section-5.2.2}. 51 * 52 * @param BigInteger $s 53 * @return bool|BigInteger 54 */ 55 private function rsavp1($s) 56 { 57 if ($s->compare(self::$zero) < 0 || $s->compare($this->modulus) > 0) { 58 return false; 59 } 60 return $this->exponentiate($s); 61 } 62 63 /** 64 * RSASSA-PKCS1-V1_5-VERIFY 65 * 66 * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.2 RFC3447#section-8.2.2}. 67 * 68 * @param string $m 69 * @param string $s 70 * @throws \LengthException if the RSA modulus is too short 71 * @return bool 72 */ 73 private function rsassa_pkcs1_v1_5_verify($m, $s) 74 { 75 // Length checking 76 77 if (strlen($s) != $this->k) { 78 return false; 79 } 80 81 // RSA verification 82 83 $s = $this->os2ip($s); 84 $m2 = $this->rsavp1($s); 85 if ($m2 === false) { 86 return false; 87 } 88 $em = $this->i2osp($m2, $this->k); 89 if ($em === false) { 90 return false; 91 } 92 93 // EMSA-PKCS1-v1_5 encoding 94 95 $exception = false; 96 97 // If the encoding operation outputs "intended encoded message length too short," output "RSA modulus 98 // too short" and stop. 99 try { 100 $em2 = $this->emsa_pkcs1_v1_5_encode($m, $this->k); 101 $r1 = hash_equals($em, $em2); 102 } catch (\LengthException $e) { 103 $exception = true; 104 } 105 106 try { 107 $em3 = $this->emsa_pkcs1_v1_5_encode_without_null($m, $this->k); 108 $r2 = hash_equals($em, $em3); 109 } catch (\LengthException $e) { 110 $exception = true; 111 } catch (UnsupportedAlgorithmException $e) { 112 $r2 = false; 113 } 114 115 if ($exception) { 116 throw new \LengthException('RSA modulus too short'); 117 } 118 119 // Compare 120 return boolval($r1 | $r2); 121 } 122 123 /** 124 * RSASSA-PKCS1-V1_5-VERIFY (relaxed matching) 125 * 126 * Per {@link http://tools.ietf.org/html/rfc3447#page-43 RFC3447#page-43} PKCS1 v1.5 127 * specified the use BER encoding rather than DER encoding that PKCS1 v2.0 specified. 128 * This means that under rare conditions you can have a perfectly valid v1.5 signature 129 * that fails to validate with _rsassa_pkcs1_v1_5_verify(). PKCS1 v2.1 also recommends 130 * that if you're going to validate these types of signatures you "should indicate 131 * whether the underlying BER encoding is a DER encoding and hence whether the signature 132 * is valid with respect to the specification given in [PKCS1 v2.0+]". so if you do 133 * $rsa->getLastPadding() and get RSA::PADDING_RELAXED_PKCS1 back instead of 134 * RSA::PADDING_PKCS1... that means BER encoding was used. 135 * 136 * @param string $m 137 * @param string $s 138 * @return bool 139 */ 140 private function rsassa_pkcs1_v1_5_relaxed_verify($m, $s) 141 { 142 // Length checking 143 144 if (strlen($s) != $this->k) { 145 return false; 146 } 147 148 // RSA verification 149 150 $s = $this->os2ip($s); 151 $m2 = $this->rsavp1($s); 152 if ($m2 === false) { 153 return false; 154 } 155 $em = $this->i2osp($m2, $this->k); 156 if ($em === false) { 157 return false; 158 } 159 160 if (Strings::shift($em, 2) != "\0\1") { 161 return false; 162 } 163 164 $em = ltrim($em, "\xFF"); 165 if (Strings::shift($em) != "\0") { 166 return false; 167 } 168 169 $decoded = ASN1::decodeBER($em); 170 if (!is_array($decoded) || empty($decoded[0]) || strlen($em) > $decoded[0]['length']) { 171 return false; 172 } 173 174 static $oids; 175 if (!isset($oids)) { 176 $oids = [ 177 'md2' => '1.2.840.113549.2.2', 178 'md4' => '1.2.840.113549.2.4', // from PKCS1 v1.5 179 'md5' => '1.2.840.113549.2.5', 180 'id-sha1' => '1.3.14.3.2.26', 181 'id-sha256' => '2.16.840.1.101.3.4.2.1', 182 'id-sha384' => '2.16.840.1.101.3.4.2.2', 183 'id-sha512' => '2.16.840.1.101.3.4.2.3', 184 // from PKCS1 v2.2 185 'id-sha224' => '2.16.840.1.101.3.4.2.4', 186 'id-sha512/224' => '2.16.840.1.101.3.4.2.5', 187 'id-sha512/256' => '2.16.840.1.101.3.4.2.6', 188 ]; 189 ASN1::loadOIDs($oids); 190 } 191 192 $decoded = ASN1::asn1map($decoded[0], DigestInfo::MAP); 193 if (!isset($decoded) || $decoded === false) { 194 return false; 195 } 196 197 if (!isset($oids[$decoded['digestAlgorithm']['algorithm']])) { 198 return false; 199 } 200 201 if (isset($decoded['digestAlgorithm']['parameters']) && $decoded['digestAlgorithm']['parameters'] !== ['null' => '']) { 202 return false; 203 } 204 205 $hash = $decoded['digestAlgorithm']['algorithm']; 206 $hash = substr($hash, 0, 3) == 'id-' ? 207 substr($hash, 3) : 208 $hash; 209 $hash = new Hash($hash); 210 $em = $hash->hash($m); 211 $em2 = $decoded['digest']; 212 213 return hash_equals($em, $em2); 214 } 215 216 /** 217 * EMSA-PSS-VERIFY 218 * 219 * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.2 RFC3447#section-9.1.2}. 220 * 221 * @param string $m 222 * @param string $em 223 * @param int $emBits 224 * @return string 225 */ 226 private function emsa_pss_verify($m, $em, $emBits) 227 { 228 // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error 229 // be output. 230 231 $emLen = ($emBits + 7) >> 3; // ie. ceil($emBits / 8); 232 $sLen = $this->sLen !== null ? $this->sLen : $this->hLen; 233 234 $mHash = $this->hash->hash($m); 235 if ($emLen < $this->hLen + $sLen + 2) { 236 return false; 237 } 238 239 if ($em[strlen($em) - 1] != chr(0xBC)) { 240 return false; 241 } 242 243 $maskedDB = substr($em, 0, -$this->hLen - 1); 244 $h = substr($em, -$this->hLen - 1, $this->hLen); 245 $temp = chr(256 - (1 << ($emBits & 7))); 246 if ((~$maskedDB[0] & $temp) != $temp) { 247 return false; 248 } 249 $dbMask = $this->mgf1($h, $emLen - $this->hLen - 1); 250 $db = $maskedDB ^ $dbMask; 251 $db[0] = ~chr(256 - (1 << ($emBits & 7))) & $db[0]; 252 $temp = $emLen - $this->hLen - $sLen - 2; 253 if (substr($db, 0, $temp) != str_repeat(chr(0), $temp) || ord($db[$temp]) != 1) { 254 return false; 255 } 256 $salt = substr($db, $temp + 1); // should be $sLen long 257 $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; 258 $h2 = $this->hash->hash($m2); 259 return hash_equals($h, $h2); 260 } 261 262 /** 263 * RSASSA-PSS-VERIFY 264 * 265 * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.2 RFC3447#section-8.1.2}. 266 * 267 * @param string $m 268 * @param string $s 269 * @return bool|string 270 */ 271 private function rsassa_pss_verify($m, $s) 272 { 273 // Length checking 274 275 if (strlen($s) != $this->k) { 276 return false; 277 } 278 279 // RSA verification 280 281 $modBits = strlen($this->modulus->toBits()); 282 283 $s2 = $this->os2ip($s); 284 $m2 = $this->rsavp1($s2); 285 $em = $this->i2osp($m2, $this->k); 286 if ($em === false) { 287 return false; 288 } 289 290 // EMSA-PSS verification 291 292 return $this->emsa_pss_verify($m, $em, $modBits - 1); 293 } 294 295 /** 296 * Verifies a signature 297 * 298 * @see self::sign() 299 * @param string $message 300 * @param string $signature 301 * @return bool 302 */ 303 public function verify($message, $signature) 304 { 305 /* 306 https://datatracker.ietf.org/doc/html/rfc4055#page-6 says the following: 307 308 There are two possible encodings for the AlgorithmIdentifier 309 parameters field associated with these object identifiers. The two 310 alternatives arise from the loss of the OPTIONAL associated with the 311 algorithm identifier parameters when the 1988 syntax for 312 AlgorithmIdentifier was translated into the 1997 syntax. Later the 313 OPTIONAL was recovered via a defect report, but by then many people 314 thought that algorithm parameters were mandatory. Because of this 315 history some implementations encode parameters as a NULL element 316 while others omit them entirely. The correct encoding is to omit the 317 parameters field; however, when RSASSA-PSS and RSAES-OAEP were 318 defined, it was done using the NULL parameters rather than absent 319 parameters. 320 321 All implementations MUST accept both NULL and absent parameters as 322 legal and equivalent encodings. 323 324 OpenSSL does NOT accept both - it REQUIRES NULL be present. phpseclib, however, 325 DOES accept both. at first, it didn't. at first, not knowing why some small number 326 of PKCS1 signatures ommitted NULL, i added the SIGNATURE_RELAXED_PKCS1 mode on 327 2015-08-26. https://phpseclib.com/docs/rsa#rsasignature_relaxed_pkcs1 talks more 328 about that mode. later, on 2021-04-05, there was CVE-2021-30130. consequently, 329 the SIGNATURE_PKCS1 mode was updated to accept either NULL or non-NULL. 330 331 because phpseclib accepts PKCS1 signatures that OpenSSL doesn't, OpenSSL isn't 332 used for PKCS1. if the OpenSSL extension is installed then it'll be used to perform 333 unpadded RSA (ie. modular exponentation), however, the actual PKCS1 construction 334 takes place in PHP code vs OpenSSL. 335 336 see https://security.stackexchange.com/questions/110330/encoding-of-optional-null-in-der 337 for an additional reference 338 */ 339 if ($this->signaturePadding === self::SIGNATURE_PKCS1 && isset(self::$forcedEngine) && self::$forcedEngine !== 'PHP') { 340 throw new BadConfigurationException('Engine OpenSSL is forced but unavailable for RSA PKCS1 signature verification'); 341 } 342 343 $result = $this->handleOpenSSL('openssl_verify', $message, $signature); 344 if ($result !== null) { 345 return $result; 346 } 347 348 switch ($this->signaturePadding) { 349 case self::SIGNATURE_RELAXED_PKCS1: 350 return $this->rsassa_pkcs1_v1_5_relaxed_verify($message, $signature); 351 case self::SIGNATURE_PKCS1: 352 return $this->rsassa_pkcs1_v1_5_verify($message, $signature); 353 //case self::SIGNATURE_PSS: 354 default: 355 return $this->rsassa_pss_verify($message, $signature); 356 } 357 } 358 359 /** 360 * RSAES-PKCS1-V1_5-ENCRYPT 361 * 362 * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}. 363 * 364 * @param string $m 365 * @param bool $pkcs15_compat optional 366 * @throws \LengthException if strlen($m) > $this->k - 11 367 * @return bool|string 368 */ 369 private function rsaes_pkcs1_v1_5_encrypt($m, $pkcs15_compat = false) 370 { 371 $mLen = strlen($m); 372 373 // Length checking 374 375 if ($mLen > $this->k - 11) { 376 throw new \LengthException('Message too long'); 377 } 378 379 // EME-PKCS1-v1_5 encoding 380 381 $psLen = $this->k - $mLen - 3; 382 $ps = ''; 383 while (strlen($ps) != $psLen) { 384 $temp = Random::string($psLen - strlen($ps)); 385 $temp = str_replace("\x00", '', $temp); 386 $ps .= $temp; 387 } 388 $type = 2; 389 $em = chr(0) . chr($type) . $ps . chr(0) . $m; 390 391 // RSA encryption 392 $m = $this->os2ip($em); 393 $c = $this->rsaep($m); 394 $c = $this->i2osp($c, $this->k); 395 396 // Output the ciphertext C 397 398 return $c; 399 } 400 401 /** 402 * RSAES-OAEP-ENCRYPT 403 * 404 * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.1 RFC3447#section-7.1.1} and 405 * {http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding OAES}. 406 * 407 * @param string $m 408 * @throws \LengthException if strlen($m) > $this->k - 2 * $this->hLen - 2 409 * @return string 410 */ 411 private function rsaes_oaep_encrypt($m) 412 { 413 $mLen = strlen($m); 414 415 // Length checking 416 417 // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error 418 // be output. 419 420 if ($mLen > $this->k - 2 * $this->hLen - 2) { 421 throw new \LengthException('Message too long'); 422 } 423 424 // EME-OAEP encoding 425 426 $lHash = $this->hash->hash($this->label); 427 $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2); 428 $db = $lHash . $ps . chr(1) . $m; 429 $seed = Random::string($this->hLen); 430 $dbMask = $this->mgf1($seed, $this->k - $this->hLen - 1); 431 $maskedDB = $db ^ $dbMask; 432 $seedMask = $this->mgf1($maskedDB, $this->hLen); 433 $maskedSeed = $seed ^ $seedMask; 434 $em = chr(0) . $maskedSeed . $maskedDB; 435 436 // RSA encryption 437 438 $m = $this->os2ip($em); 439 $c = $this->rsaep($m); 440 $c = $this->i2osp($c, $this->k); 441 442 // Output the ciphertext C 443 444 return $c; 445 } 446 447 /** 448 * RSAEP 449 * 450 * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}. 451 * 452 * @param BigInteger $m 453 * @return bool|BigInteger 454 */ 455 private function rsaep($m) 456 { 457 if ($m->compare(self::$zero) < 0 || $m->compare($this->modulus) > 0) { 458 throw new \OutOfRangeException('Message representative out of range'); 459 } 460 return $this->exponentiate($m); 461 } 462 463 /** 464 * Raw Encryption / Decryption 465 * 466 * Doesn't use padding and is not recommended. 467 * 468 * @param string $m 469 * @return bool|string 470 * @throws \LengthException if strlen($m) > $this->k 471 */ 472 private function raw_encrypt($m) 473 { 474 if (strlen($m) > $this->k) { 475 throw new \LengthException('Message too long'); 476 } 477 478 $temp = $this->os2ip($m); 479 $temp = $this->rsaep($temp); 480 return $this->i2osp($temp, $this->k); 481 } 482 483 /** 484 * Encryption 485 * 486 * Both self::PADDING_OAEP and self::PADDING_PKCS1 both place limits on how long $plaintext can be. 487 * If $plaintext exceeds those limits it will be broken up so that it does and the resultant ciphertext's will 488 * be concatenated together. 489 * 490 * @see self::decrypt() 491 * @param string $plaintext 492 * @return bool|string 493 * @throws \LengthException if the RSA modulus is too short 494 */ 495 public function encrypt($plaintext) 496 { 497 $result = $this->handleOpenSSL('openssl_public_encrypt', $plaintext); 498 if ($result !== null) { 499 return $result; 500 } 501 502 switch ($this->encryptionPadding) { 503 case self::ENCRYPTION_NONE: 504 return $this->raw_encrypt($plaintext); 505 case self::ENCRYPTION_PKCS1: 506 return $this->rsaes_pkcs1_v1_5_encrypt($plaintext); 507 //case self::ENCRYPTION_OAEP: 508 default: 509 return $this->rsaes_oaep_encrypt($plaintext); 510 } 511 } 512 513 /** 514 * Returns the public key 515 * 516 * The public key is only returned under two circumstances - if the private key had the public key embedded within it 517 * or if the public key was set via setPublicKey(). If the currently loaded key is supposed to be the public key this 518 * function won't return it since this library, for the most part, doesn't distinguish between public and private keys. 519 * 520 * @param string $type 521 * @param array $options optional 522 * @return mixed 523 */ 524 public function toString($type, array $options = []) 525 { 526 $type = self::validatePlugin('Keys', $type, 'savePublicKey'); 527 528 if ($type == PSS::class) { 529 if ($this->signaturePadding == self::SIGNATURE_PSS) { 530 $options += [ 531 'hash' => $this->hash->getHash(), 532 'MGFHash' => $this->mgfHash->getHash(), 533 'saltLength' => $this->getSaltLength() 534 ]; 535 } else { 536 throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS'); 537 } 538 } 539 540 return $type::savePublicKey($this->modulus, $this->publicExponent, $options); 541 } 542 543 /** 544 * Converts a public key to a private key 545 * 546 * @return RSA 547 */ 548 public function asPrivateKey() 549 { 550 $new = new PrivateKey(); 551 $new->exponent = $this->exponent; 552 $new->modulus = $this->modulus; 553 $new->k = $this->k; 554 $new->format = $this->format; 555 return $new 556 ->withHash($this->hash->getHash()) 557 ->withMGFHash($this->mgfHash->getHash()) 558 ->withSaltLength($this->sLen) 559 ->withLabel($this->label) 560 ->withPadding($this->signaturePadding | $this->encryptionPadding); 561 } 562} 563