From 8e03f5bfb290318de857a98562fc7a0ccae331c5 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sat, 1 Jun 2019 19:35:17 -0500 Subject: [PATCH] RSA: make it so PSS keys can be saved --- phpseclib/Crypt/RSA.php | 25 +++-- phpseclib/Crypt/RSA/Keys/PSS.php | 101 +++++++++++++++++- phpseclib/Crypt/RSA/PrivateKey.php | 15 ++- phpseclib/Crypt/RSA/PublicKey.php | 13 +++ .../File/ASN1/Maps/RSASSA_PSS_params.php | 4 +- tests/Unit/Crypt/RSA/LoadKeyTest.php | 10 ++ 6 files changed, 154 insertions(+), 14 deletions(-) diff --git a/phpseclib/Crypt/RSA.php b/phpseclib/Crypt/RSA.php index 096972bf..78d25406 100644 --- a/phpseclib/Crypt/RSA.php +++ b/phpseclib/Crypt/RSA.php @@ -53,6 +53,7 @@ use phpseclib\Crypt\RSA\PublicKey; use phpseclib\Math\BigInteger; use phpseclib\Exceptions\UnsupportedAlgorithmException; use phpseclib\Exceptions\InconsistentSetupException; +use phpseclib\Crypt\RSA\Keys\PSS; /** * Pure-PHP PKCS#1 compliant implementation of RSA. @@ -405,14 +406,26 @@ abstract class RSA extends AsymmetricKey if ($components['isPublicKey']) { $key->exponent = $key->publicExponent; - return $key; + } else { + $key->privateExponent = $components['privateExponent']; + $key->exponent = $key->privateExponent; + $key->primes = $components['primes']; + $key->exponents = $components['exponents']; + $key->coefficients = $components['coefficients']; } - $key->privateExponent = $components['privateExponent']; - $key->exponent = $key->privateExponent; - $key->primes = $components['primes']; - $key->exponents = $components['exponents']; - $key->coefficients = $components['coefficients']; + if ($components['format'] == PSS::class) { + $key = $key->withPadding(self::SIGNATURE_PSS); + if (isset($components['hash'])) { + $key = $key->withHash($components['hash']); + } + if (isset($components['MGFHash'])) { + $key = $key->withMGFHash($components['MGFHash']); + } + if (isset($components['saltLength'])) { + $key = $key->withSaltLength($components['saltLength']); + } + } return $key; } diff --git a/phpseclib/Crypt/RSA/Keys/PSS.php b/phpseclib/Crypt/RSA/Keys/PSS.php index 4246a367..2b759984 100644 --- a/phpseclib/Crypt/RSA/Keys/PSS.php +++ b/phpseclib/Crypt/RSA/Keys/PSS.php @@ -9,11 +9,11 @@ * * Processes keys with the following headers: * + * -----BEGIN ENCRYPTED PRIVATE KEY----- + * -----BEGIN PRIVATE KEY----- * -----BEGIN PUBLIC KEY----- * - * Analogous to ssh-keygen's pkcs8 format (as specified by -m). Although PKCS8 - * is specific to private keys it's basically creating a DER-encoded wrapper - * for keys. This just extends that same concept to public keys (much like ssh-keygen) + * Analogous to "openssl genpkey -algorithm rsa-pss". * * @category Crypt * @package RSA @@ -28,6 +28,7 @@ namespace phpseclib\Crypt\RSA\Keys; use phpseclib\Math\BigInteger; use phpseclib\Crypt\Common\Keys\PKCS8 as Progenitor; use phpseclib\File\ASN1; +use phpseclib\File\ASN1\Maps; /** * PKCS#8 Formatted RSA-PSS Key Handler @@ -122,13 +123,13 @@ abstract class PSS extends Progenitor if ($decoded === false) { throw new \UnexpectedValueException('Unable to decode parameters'); } - $params = ASN1::asn1map($decoded[0], ASN1\Maps\RSASSA_PSS_params::MAP); + $params = ASN1::asn1map($decoded[0], Maps\RSASSA_PSS_params::MAP); if (isset($params['maskGenAlgorithm']['parameters'])) { $decoded = ASN1::decodeBER($params['maskGenAlgorithm']['parameters']); if ($decoded === false) { throw new \UnexpectedValueException('Unable to decode parameters'); } - $params['maskGenAlgorithm']['parameters'] = ASN1::asn1map($decoded[0], ASN1\Maps\HashAlgorithm::MAP); + $params['maskGenAlgorithm']['parameters'] = ASN1::asn1map($decoded[0], Maps\HashAlgorithm::MAP); } else { $params['maskGenAlgorithm'] = [ 'algorithm' => 'id-mgf1', @@ -136,6 +137,10 @@ abstract class PSS extends Progenitor ]; } + if (!isset($params['hashAlgorithm']['algorithm'])) { + $params['hashAlgorithm']['algorithm'] = 'id-sha1'; + } + $result['hash'] = str_replace('id-', '', $params['hashAlgorithm']['algorithm']); $result['MGFHash'] = str_replace('id-', '', $params['maskGenAlgorithm']['parameters']['algorithm']); $result['saltLength'] = (int) $params['saltLength']->toString(); @@ -146,4 +151,90 @@ abstract class PSS extends Progenitor return $result; } + + /** + * Convert a private key to the appropriate format. + * + * @access public + * @param \phpseclib\Math\BigInteger $n + * @param \phpseclib\Math\BigInteger $e + * @param \phpseclib\Math\BigInteger $d + * @param array $primes + * @param array $exponents + * @param array $coefficients + * @param string $password optional + * @param array $options optional + * @return string + */ + public static function savePrivateKey(BigInteger $n, BigInteger $e, BigInteger $d, $primes, $exponents, $coefficients, $password = '', $options = []) + { + self::initialize_static_variables(); + + $key = PKCS1::savePrivateKey($n, $e, $d, $primes, $exponents, $coefficients); + $key = ASN1::extractBER($key); + $params = self::savePSSParams($options); + return self::wrapPrivateKey($key, [], $params, $password, $options); + } + + /** + * Convert a public key to the appropriate format + * + * @access public + * @param \phpseclib\Math\BigInteger $n + * @param \phpseclib\Math\BigInteger $e + * @param array $options optional + * @return string + */ + public static function savePublicKey(BigInteger $n, BigInteger $e, $options = []) + { + $key = PKCS1::savePublicKey($n, $e); + $key = ASN1::extractBER($key); + $params = self::savePSSParams($options); + return self::wrapPublicKey($key, $params); + } + + /** + * Encodes PSS parameters + * + * @access public + * @param array $options + * @return string + */ + private static function savePSSParams($options) + { + /* + The trailerField field is an integer. It provides + compatibility with IEEE Std 1363a-2004 [P1363A]. The value + MUST be 1, which represents the trailer field with hexadecimal + value 0xBC. Other trailer fields, including the trailer field + composed of HashID concatenated with 0xCC that is specified in + IEEE Std 1363a, are not supported. Implementations that + perform signature generation MUST omit the trailerField field, + indicating that the default trailer field value was used. + Implementations that perform signature validation MUST + recognize both a present trailerField field with value 1 and an + absent trailerField field. + + source: https://tools.ietf.org/html/rfc4055#page-9 + */ + $params = [ + 'trailerField' => new BigInteger(1) + ]; + if (isset($options['hash'])) { + $params['hashAlgorithm']['algorithm'] = 'id-' . $options['hash']; + } + if (isset($options['MGFHash'])) { + $temp = ['algorithm' => 'id-' . $options['MGFHash']]; + $temp = ASN1::encodeDER($temp, Maps\HashAlgorithm::MAP); + $params['maskGenAlgorithm'] = [ + 'algorithm' => 'id-mgf1', + 'parameters' => new ASN1\Element($temp) + ]; + } + if (isset($options['saltLength'])) { + $params['saltLength'] = new BigInteger($options['saltLength']); + } + + return new ASN1\Element(ASN1::encodeDER($params, Maps\RSASSA_PSS_params::MAP)); + } } diff --git a/phpseclib/Crypt/RSA/PrivateKey.php b/phpseclib/Crypt/RSA/PrivateKey.php index 177ef9b9..5788f5b5 100644 --- a/phpseclib/Crypt/RSA/PrivateKey.php +++ b/phpseclib/Crypt/RSA/PrivateKey.php @@ -22,6 +22,7 @@ use phpseclib\Exceptions\NoKeyLoadedException; use phpseclib\Exception\UnsupportedFormatException; use phpseclib\Crypt\Random; use phpseclib\Crypt\Common; +use phpseclib\Crypt\RSA\Keys\PSS; /** * Raw RSA Key Handler @@ -524,9 +525,21 @@ class PrivateKey extends RSA implements Common\PrivateKey $type, empty($this->primes) ? 'savePublicKey' : 'savePrivateKey' ); + + if ($type == PSS::class) { + if ($this->signaturePadding == self::SIGNATURE_PSS) { + $options+= [ + 'hash' => $this->hash->getHash(), + 'MGFHash' => $this->mgfHash->getHash(), + 'saltLength' => $this->sLen + ]; + } else { + throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS'); + } + } if (empty($this->primes)) { - return $type::savePublicKey($this->modulus, $this->exponent); + return $type::savePublicKey($this->modulus, $this->exponent, $options); } return $type::savePrivateKey($this->modulus, $this->publicExponent, $this->exponent, $this->primes, $this->exponents, $this->coefficients, $this->password, $options); diff --git a/phpseclib/Crypt/RSA/PublicKey.php b/phpseclib/Crypt/RSA/PublicKey.php index 66cc091e..50233633 100644 --- a/phpseclib/Crypt/RSA/PublicKey.php +++ b/phpseclib/Crypt/RSA/PublicKey.php @@ -22,6 +22,7 @@ use phpseclib\Exceptions\NoKeyLoadedException; use phpseclib\Crypt\Random; use phpseclib\Crypt\Common; use phpseclib\File\ASN1\Maps\DigestInfo; +use phpseclib\Crypt\RSA\Keys\PSS; /** * Raw RSA Key Handler @@ -472,6 +473,18 @@ class PublicKey extends RSA implements Common\PublicKey { $type = self::validatePlugin('Keys', $type, 'savePublicKey'); + if ($type == PSS::class) { + if ($this->signaturePadding == self::SIGNATURE_PSS) { + $options+= [ + 'hash' => $this->hash->getHash(), + 'MGFHash' => $this->mgfHash->getHash(), + 'saltLength' => $this->sLen + ]; + } else { + throw new UnsupportedFormatException('The PSS format can only be used when the signature method has been explicitly set to PSS'); + } + } + return $type::savePublicKey($this->modulus, $this->publicExponent, $options); } diff --git a/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php b/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php index cbd8d750..12d72d24 100644 --- a/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php +++ b/phpseclib/File/ASN1/Maps/RSASSA_PSS_params.php @@ -35,13 +35,13 @@ abstract class RSASSA_PSS_params 'constant' => 0, 'optional' => true, 'explicit' => true, - 'default' => 'sha1Identifier' + //'default' => 'sha1Identifier' ] + HashAlgorithm::MAP, 'maskGenAlgorithm' => [ 'constant' => 1, 'optional' => true, 'explicit' => true, - 'default' => 'mgf1SHA1Identifier' + //'default' => 'mgf1SHA1Identifier' ] + MaskGenAlgorithm::MAP, 'saltLength' => [ 'type' => ASN1::TYPE_INTEGER, diff --git a/tests/Unit/Crypt/RSA/LoadKeyTest.php b/tests/Unit/Crypt/RSA/LoadKeyTest.php index d7b55baa..66cb5ff6 100644 --- a/tests/Unit/Crypt/RSA/LoadKeyTest.php +++ b/tests/Unit/Crypt/RSA/LoadKeyTest.php @@ -13,6 +13,7 @@ use phpseclib\Crypt\RSA\Keys\PKCS1; use phpseclib\Crypt\RSA\Keys\PKCS8; use phpseclib\Crypt\RSA\Keys\PuTTY; use phpseclib\Crypt\RSA\Keys\OpenSSH; +use phpseclib\Crypt\RSA\Keys\PSS; use phpseclib\Math\BigInteger; class Unit_Crypt_RSA_LoadKeyTest extends PhpseclibTestCase @@ -914,5 +915,14 @@ IBgv3a3Lyb+IQtT75LE1yjE= $rsa = PublicKeyLoader::load($key); $this->assertInstanceOf(PrivateKey::class, $rsa); $this->assertInstanceOf(PublicKey::class, $rsa->getPublicKey()); + + $r = PSS::load($key); + + $key = $rsa->toString('PSS'); + $r2 = PSS::load($key); + + $this->assertSame($r['hash'], $r2['hash']); + $this->assertSame($r['MGFHash'], $r2['MGFHash']); + $this->assertSame($r['saltLength'], $r2['saltLength']); } }