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