diff --git a/phpseclib/Crypt/Common/Keys/OpenSSH.php b/phpseclib/Crypt/Common/Keys/OpenSSH.php index c1b43fa3..5d65dc1a 100644 --- a/phpseclib/Crypt/Common/Keys/OpenSSH.php +++ b/phpseclib/Crypt/Common/Keys/OpenSSH.php @@ -19,6 +19,7 @@ namespace phpseclib\Crypt\Common\Keys; use ParagonIE\ConstantTime\Base64; use phpseclib\Common\Functions\Strings; +use phpseclib\Crypt\Random; /** * OpenSSH Formatted RSA Key Handler @@ -66,50 +67,99 @@ abstract class OpenSSH * @param string $type * @return array */ - public static function load($key, $type) + public static function load($key, $password = '') { if (!is_string($key)) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } + // key format is described here: + // https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD + + if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) { + $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key); + $key = Base64::decode($key); + $magic = Strings::shift($key, 15); + if ($magic != "openssh-key-v1\0") { + throw new \RuntimeException('Expected openssh-key-v1'); + } + list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key); + if ($numKeys != 1) { + // if we wanted to support multiple keys we could update PublicKeyLoader to preview what the # of keys + // would be; it'd then call Common\Keys\OpenSSH.php::load() and get the paddedKey. it'd then pass + // that to the appropriate key loading parser $numKey times or something + throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); + } + if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') { + /* + OpenSSH private keys use a customized version of bcrypt. specifically, instead of encrypting + OrpheanBeholderScryDoubt 64 times OpenSSH's bcrypt variant encrypts + OxychromaticBlowfishSwatDynamite 64 times. so we can't use crypt(). + + bcrypt is basically Blowfish with an altered key expansion. whereas Blowfish just runs the + key through the key expansion bcrypt interleaves the key expansion with the salt and + password. this renders openssl / mcrypt unusuable. this forces us to use a pure-PHP implementation + of bcrypt. the problem with that is that pure-PHP is too slow to be practically useful. + + in addition to encrypting a different string 64 times the OpenSSH implementation also performs bcrypt + from scratch $rounds times. calling crypt() 64x with bcrypt takes 0.7s. PHP is going to be naturally + slower. pure-PHP is 215x slower than OpenSSL for AES and pure-PHP is 43x slower for bcrypt. + 43 * 0.7 = 30s. no one wants to wait 30s to load a private key. + + another way to think about this.. according to wikipedia's article on Blowfish, + "Each new key requires pre-processing equivalent to encrypting about 4 kilobytes of text". + key expansion is done (9+64*2)*160 times. multiply that by 4 and it turns out that Blowfish, + OpenSSH style, is the equivalent of encrypting ~80mb of text. + + more supporting evidence: sodium_compat does not implement Argon2 (another password hashing + algorithm) because "It's not feasible to polyfill scrypt or Argon2 into PHP and get reasonable + performance. Users would feel motivated to select parameters that downgrade security to avoid + denial of service (DoS) attacks. The only winning move is not to play" + -- https://github.com/paragonie/sodium_compat/blob/master/README.md + */ + throw new \RuntimeException('Encrypted OpenSSH private keys are not supported'); + //list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); + } + + list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); + list($type) = Strings::unpackSSH2('s', $publicKey); + list($checkint1, $checkint2) = Strings::unpackSSH2('NN', $paddedKey); + // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc. + if ($checkint1 != $checkint2) { + throw new \RuntimeException('The two checkints do not match'); + } + self::checkType($type); + + return compact('type', 'publicKey', 'paddedKey'); + } + $parts = explode(' ', $key, 3); if (!isset($parts[1])) { - $key = Base64::decode($parts[0]); + $key = base64_decode($parts[0]); $comment = isset($parts[1]) ? $parts[1] : false; } else { - if ($parts[0] != $type) { - throw new \UnexpectedValueException('Expected a ' . $type . ' key - got a ' . $parts[0] . ' key'); - } - $key = Base64::decode($parts[1]); + $asciiType = $parts[0]; + self::checkType($parts[0]); + $key = base64_decode($parts[1]); $comment = isset($parts[2]) ? $parts[2] : false; } if ($key === false) { throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); } - if (Strings::shift($key, strlen($type) + 4) != "\0\0\0" . chr(strlen($type)) . $type) { - throw new \UnexpectedValueException('Key appears to be malformed'); + list($type) = Strings::unpackSSH2('s', $key); + self::checkType($type); + if (isset($asciiType) && $asciiType != $type) { + throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $type); } if (strlen($key) <= 4) { throw new \UnexpectedValueException('Key appears to be malformed'); } - return $key; - } + $publicKey = $key; - /** - * Returns the comment for the key - * - * @access public - * @param string $key - * @return mixed - */ - public static function getComment($key) - { - $parts = explode(' ', $key, 3); - - return isset($parts[2]) ? $parts[2] : false; + return compact('type', 'publicKey', 'comment'); } /** @@ -125,4 +175,53 @@ abstract class OpenSSH { self::$binary = $enabled; } + + /** + * Checks to see if the type is valid + * + * @access private + * @param string $candidate + */ + private static function checkType($candidate) + { + if (!in_array($candidate, static::$types)) { + throw new \RuntimeException('The key type is not equal to: ' . implode(',', static::$types)); + } + } + + /** + * Wrap a private key appropriately + * + * @access public + * @param string $publicKey + * @param string $privateKey + * @return string + */ + protected static function wrapPrivateKey($publicKey, $privateKey, $options) + { + list(, $checkint) = unpack('N', Random::string(4)); + + $comment = isset($options['comment']) ? $options['comment'] : self::$comment; + $paddedKey = Strings::packSSH2('NN', $checkint, $checkint) . + $privateKey . + Strings::packSSH2('s', $comment); + + /* + from http://tools.ietf.org/html/rfc4253#section-6 : + + Note that the length of the concatenation of 'packet_length', + 'padding_length', 'payload', and 'random padding' MUST be a multiple + of the cipher block size or 8, whichever is larger. + */ + $paddingLength = (7 * strlen($paddedKey)) % 8; + for ($i = 1; $i <= $paddingLength; $i++) { + $paddedKey.= chr($i); + } + $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); + $key = "openssh-key-v1\0$key"; + + return "-----BEGIN OPENSSH PRIVATE KEY-----\r\n" . + chunk_split(Base64::encode($key), 70) . + "-----END OPENSSH PRIVATE KEY-----"; + } } diff --git a/phpseclib/Crypt/DSA/Keys/OpenSSH.php b/phpseclib/Crypt/DSA/Keys/OpenSSH.php index 16b2c453..1a06b1d0 100644 --- a/phpseclib/Crypt/DSA/Keys/OpenSSH.php +++ b/phpseclib/Crypt/DSA/Keys/OpenSSH.php @@ -31,6 +31,13 @@ use phpseclib\Crypt\Common\Keys\OpenSSH as Progenitor; */ abstract class OpenSSH extends Progenitor { + /** + * Supported Key Types + * + * @var array + */ + protected static $types = ['ssh-dss']; + /** * Break a public or private key down into its constituent components * @@ -41,15 +48,22 @@ abstract class OpenSSH extends Progenitor */ public static function load($key, $password = '') { - $key = parent::load($key, 'ssh-dss'); + $parsed = parent::load($key, 'ssh-dss'); - $result = Strings::unpackSSH2('iiii', $key); - if ($result === false) { - throw new \UnexpectedValueException('Key appears to be malformed'); + if (isset($parsed['paddedKey'])) { + list($type) = Strings::unpackSSH2('s', $parsed['paddedKey']); + if ($type != $parsed['type']) { + throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); + } + + list($p, $q, $g, $y, $x, $comment) = Strings::unpackSSH2('i5s', $parsed['paddedKey']); + + return compact('p', 'q', 'g', 'y', 'x', 'comment'); } - list($p, $q, $g, $y) = $result; - $comment = parent::getComment($key); + list($p, $q, $g, $y) = Strings::unpackSSH2('iiii', $parsed['publicKey']); + + $comment = $parsed['comment']; return compact('p', 'q', 'g', 'y', 'comment'); } @@ -84,8 +98,29 @@ abstract class OpenSSH extends Progenitor } $comment = isset($options['comment']) ? $options['comment'] : self::$comment; - $DSAPublicKey = 'ssh-dss ' . Base64::encode($DSAPublicKey) . ' ' . $comment; + $DSAPublicKey = 'ssh-dss ' . base64_encode($DSAPublicKey) . ' ' . $comment; return $DSAPublicKey; } + + /** + * Convert a private key to the appropriate format. + * + * @access public + * @param \phpseclib\Math\BigInteger $p + * @param \phpseclib\Math\BigInteger $q + * @param \phpseclib\Math\BigInteger $g + * @param \phpseclib\Math\BigInteger $x + * @param \phpseclib\Math\BigInteger $y + * @param string $password optional + * @param array $options optional + * @return string + */ + public static function savePrivateKey(BigInteger $p, BigInteger $q, BigInteger $g, BigInteger $y, BigInteger $x, $password = '', array $options = []) + { + $publicKey = self::savePublicKey($p, $q, $g, $y, ['binary' => true]); + $privateKey = Strings::packSSH2('si5', 'ssh-dss', $p, $q, $g, $y, $x); + + return self::wrapPrivateKey($publicKey, $privateKey, $options); + } } diff --git a/phpseclib/Crypt/DSA/Keys/PuTTY.php b/phpseclib/Crypt/DSA/Keys/PuTTY.php index 04865474..8801148b 100644 --- a/phpseclib/Crypt/DSA/Keys/PuTTY.php +++ b/phpseclib/Crypt/DSA/Keys/PuTTY.php @@ -27,7 +27,7 @@ use phpseclib\Crypt\Common\Keys\PuTTY as Progenitor; /** * PuTTY Formatted DSA Key Handler * - * @package RSA + * @package DSA * @author Jim Wigginton * @access public */ @@ -66,17 +66,8 @@ abstract class PuTTY extends Progenitor extract($components); unset($components['public'], $components['private']); - $result = Strings::unpackSSH2('iiii', $public); - if ($result === false) { - throw new \UnexpectedValueException('Key appears to be malformed'); - } - list($p, $q, $g, $y) = $result; - - $result = Strings::unpackSSH2('i', $private); - if ($result === false) { - throw new \UnexpectedValueException('Key appears to be malformed'); - } - list($x) = $result; + list($p, $q, $g, $y) = Strings::unpackSSH2('iiii', $public); + list($x) = Strings::unpackSSH2('i', $private); return compact('p', 'q', 'g', 'y', 'x', 'comment'); } diff --git a/phpseclib/Crypt/ECDSA/Keys/OpenSSH.php b/phpseclib/Crypt/ECDSA/Keys/OpenSSH.php index 8d8f9c1f..3a64f84b 100644 --- a/phpseclib/Crypt/ECDSA/Keys/OpenSSH.php +++ b/phpseclib/Crypt/ECDSA/Keys/OpenSSH.php @@ -25,7 +25,6 @@ use phpseclib\Crypt\ECDSA\BaseCurves\Base as BaseCurve; use phpseclib\Exception\UnsupportedCurveException; use phpseclib\Crypt\ECDSA\Curves\Ed25519; use phpseclib\Math\Common\FiniteField\Integer; -use phpseclib\Crypt\Random; /** * OpenSSH Formatted ECDSA Key Handler @@ -43,7 +42,7 @@ abstract class OpenSSH extends Progenitor * * @var array */ - private static $types = [ + protected static $types = [ 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', @@ -60,113 +59,39 @@ abstract class OpenSSH extends Progenitor */ public static function load($key, $password = '') { - /* - key format is described here: - https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD + $parsed = parent::load($key, $password); - this is only supported for ECDSA because of Ed25519. ssh-keygen doesn't generate a - PKCS1/8 formatted private key for Ed25519 - it generates an OpenSSH formatted - private key. probably because, at the time of this writing, there's not an actual - IETF RFC describing an Ed25519 format - */ - if (strpos($key, 'BEGIN OPENSSH PRIVATE KEY') !== false) { - $key = preg_replace('#(?:^-.*?-[\r\n]*$)|\s#ms', '', $key); - $key = Base64::decode($key); - $magic = Strings::shift($key, 15); - if ($magic != "openssh-key-v1\0") { - throw new \RuntimeException('Expected openssh-key-v1'); + if (isset($parsed['paddedKey'])) { + $paddedKey = $parsed['paddedKey']; + list($type) = Strings::unpackSSH2('s', $paddedKey); + if ($type != $parsed['type']) { + throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); } - list($ciphername, $kdfname, $kdfoptions, $numKeys) = Strings::unpackSSH2('sssN', $key); - if ($numKeys != 1) { - throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); + if ($type == 'ssh-ed25519' ) { + list(, $key, $comment) = Strings::unpackSSH2('sss', $paddedKey); + $key = libsodium::load($key); + $key['comment'] = $comment; + return $key; } - if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') { - /* - OpenSSH private keys use a customized version of bcrypt. specifically, instead of encrypting - OrpheanBeholderScryDoubt 64 times OpenSSH's bcrypt variant encrypts - OxychromaticBlowfishSwatDynamite 64 times. so we can't use crypt(). - - bcrypt is basically Blowfish with an altered key expansion. whereas Blowfish just runs the - key through the key expansion bcrypt interleaves the key expansion with the salt and - password. this renders openssl / mcrypt unusuable. this forces us to use a pure-PHP implementation - of bcrypt. the problem with that is that pure-PHP is too slow to be practically useful. - - in addition to encrypting a different string 64 times the OpenSSH also performs bcrypt from - scratch $rounds times. calling crypt() 64x with bcrypt takes 0.7s. PHP is going to be naturally - slower. pure-PHP is 215x slower than OpenSSL for AES and pure-PHP is 43x slower for bcrypt. - 43 * 0.7 = 30s. no one wants to wait 30s to load a private key. - - another way to think about this.. according to wikipedia's article on Blowfish, - "Each new key requires pre-processing equivalent to encrypting about 4 kilobytes of text". - key expansion is done (9+64*2)*160 times. multiply that by 4 and it turns out that Blowfish, - OpenSSH style, is the equivalent of encrypting ~80mb of text. - - more supporting evidence: sodium_compat does not implement Argon2 (another password hashing - algorithm) because "It's not feasible to polyfill scrypt or Argon2 into PHP and get reasonable - performance. Users would feel motivated to select parameters that downgrade security to avoid - denial of service (DoS) attacks. The only winning move is not to play" - -- https://github.com/paragonie/sodium_compat/blob/master/README.md - */ - throw new \RuntimeException('Encrypted OpenSSH private keys are not supported'); - //list($salt, $rounds) = Strings::unpackSSH2('sN', $kdfoptions); - } - - list($publicKey, $paddedKey) = Strings::unpackSSH2('ss', $key); - list($type, $publicKey) = Strings::unpackSSH2('ss', $publicKey); - if ($type != 'ssh-ed25519') { - throw new UnsupportedCurveException('ssh-ed25519 is the only supported curve for OpenSSH public keys'); - } - list($checkint1, $checkint2, $type, $publicKey2, $privateKey, $comment) = Strings::unpackSSH2('NNssss', $paddedKey); - // any leftover bytes in $paddedKey are for padding? but they should be sequential bytes. eg. 1, 2, 3, etc. - if ($checkint1 != $checkint2) { - throw new \RuntimeException('The two checkints do not match'); - } - if ($type != 'ssh-ed25519') { - throw new UnsupportedCurveException('ssh-ed25519 is the only supported curve for OpenSSH private keys'); - } - if ($publicKey != $publicKey2 || $publicKey2 != substr($privateKey, 32)) { - throw new \RuntimeException('The public keys do not match up'); - } - $privateKey = substr($privateKey, 0, 32); - $curve = new Ed25519(); + list($curveName, $publicKey, $privateKey, $comment) = Strings::unpackSSH2('ssis', $paddedKey); + $curve = self::loadCurveByParam(['namedCurve' => $curveName]); return [ 'curve' => $curve, - 'dA' => $curve->extractSecret($privateKey), - 'QA' => self::extractPoint($publicKey, $curve), + 'dA' => $curve->convertInteger($privateKey), + 'QA' => self::extractPoint("\0$publicKey", $curve), 'comment' => $comment ]; } - $parts = explode(' ', $key, 3); - - if (!isset($parts[1])) { - $key = Base64::decode($parts[0]); - $comment = isset($parts[1]) ? $parts[1] : false; - } else { - $asciiType = $parts[0]; - if (!in_array($asciiType, self::$types)) { - throw new \RuntimeException('Keys of type ' . $asciiType . ' are not supported'); - } - $key = Base64::decode($parts[1]); - $comment = isset($parts[2]) ? $parts[2] : false; - } - - list($binaryType) = Strings::unpackSSH2('s', $key); - if (isset($asciiType) && $asciiType != $binaryType) { - throw new \RuntimeException('Two different types of keys are claimed: ' . $asciiType . ' and ' . $binaryType); - } elseif (!isset($asciiType) && !in_array($binaryType, self::$types)) { - throw new \RuntimeException('Keys of type ' . $binaryType . ' are not supported'); - } - - if ($binaryType == 'ssh-ed25519') { - if (Strings::shift($key, 4) != "\0\0\0\x20") { + if ($parsed['type'] == 'ssh-ed25519') { + if (Strings::shift($parsed['publicKey'], 4) != "\0\0\0\x20") { throw new \RuntimeException('Length of ssh-ed25519 key should be 32'); } $curve = new Ed25519(); - $qa = self::extractPoint($key, $curve); + $qa = self::extractPoint($parsed['publicKey'], $curve); } else { - list($curveName, $publicKey) = Strings::unpackSSH2('ss', $key); + list($curveName, $publicKey) = Strings::unpackSSH2('ss', $parsed['publicKey']); $curveName = '\phpseclib\Crypt\ECDSA\Curves\\' . $curveName; $curve = new $curveName(); @@ -176,34 +101,17 @@ abstract class OpenSSH extends Progenitor return [ 'curve' => $curve, 'QA' => $qa, - 'comment' => $comment + 'comment' => $parsed['comment'] ]; } /** - * Convert an ECDSA public key to the appropriate format + * Returns the alias that corresponds to a curve * - * @access public - * @param \phpseclib\Crypt\ECDSA\BaseCurves\Base $curve - * @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey - * @param array $options optional * @return string */ - public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) + private static function getAlias(BaseCurve $curve) { - $comment = isset($options['comment']) ? $options['comment'] : self::$comment; - - if ($curve instanceof Ed25519) { - $key = Strings::packSSH2('ss', 'ssh-ed25519', $curve->encodePoint($publicKey)); - - if (self::$binary) { - return $key; - } - - $key = 'ssh-ed25519 ' . Base64::encode($key) . ' ' . $comment; - return $key; - } - self::initialize_static_variables(); $reflect = new \ReflectionClass($curve); @@ -226,6 +134,35 @@ abstract class OpenSSH extends Progenitor throw new UnsupportedCurveException($name . ' is not a curve that the OpenSSH plugin supports'); } + return $alias; + } + + /** + * Convert an ECDSA public key to the appropriate format + * + * @access public + * @param \phpseclib\Crypt\ECDSA\BaseCurves\Base $curve + * @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey + * @param array $options optional + * @return string + */ + public static function savePublicKey(BaseCurve $curve, array $publicKey, array $options = []) + { + $comment = isset($options['comment']) ? $options['comment'] : self::$comment; + + if ($curve instanceof Ed25519) { + $key = Strings::packSSH2('ss', 'ssh-ed25519', $curve->encodePoint($publicKey)); + + if (self::$binary) { + return $key; + } + + $key = 'ssh-ed25519 ' . base64_encode($key) . ' ' . $comment; + return $key; + } + + $alias = self::getAlias($curve); + $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); $key = Strings::packSSH2('sss', 'ecdsa-sha2-' . $alias, $alias, $points); @@ -233,7 +170,7 @@ abstract class OpenSSH extends Progenitor return $key; } - $key = 'ecdsa-sha2-' . $alias . ' ' . Base64::encode($key) . ' ' . $comment; + $key = 'ecdsa-sha2-' . $alias . ' ' . base64_encode($key) . ' ' . $comment; return $key; } @@ -246,38 +183,34 @@ abstract class OpenSSH extends Progenitor * @param \phpseclib\Crypt\ECDSA\Curves\Ed25519 $curve * @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey * @param string $password optional + * @param array $options optional * @return string */ - public static function savePrivateKey(Integer $privateKey, Ed25519 $curve, array $publicKey, $password = '') + public static function savePrivateKey(Integer $privateKey, BaseCurve $curve, array $publicKey, $password = '', array $options = []) { - if (!isset($privateKey->secret)) { - throw new \RuntimeException('Private Key does not have a secret set'); - } - if (strlen($privateKey->secret) != 32) { - throw new \RuntimeException('Private Key secret is not of the correct length'); + if ($curve instanceof Ed25519) { + if (!isset($privateKey->secret)) { + throw new \RuntimeException('Private Key does not have a secret set'); + } + if (strlen($privateKey->secret) != 32) { + throw new \RuntimeException('Private Key secret is not of the correct length'); + } + + $pubKey = $curve->encodePoint($publicKey); + + $publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey); + $privateKey = Strings::packSSH2('sss', 'ssh-ed25519', $pubKey, $privateKey->secret . $pubKey); + + return self::wrapPrivateKey($publicKey, $privateKey, $options); } - list(, $checkint) = unpack('N', Random::string(4)); - $pubKey = $curve->encodePoint($publicKey); + $alias = self::getAlias($curve); - $publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey); - $paddedKey = Strings::packSSH2('NNssss', $checkint, $checkint, 'ssh-ed25519', $pubKey, $privateKey->secret . $pubKey, self::$comment); - /* - from http://tools.ietf.org/html/rfc4253#section-6 : + $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes(); + $publicKey = self::savePublicKey($curve, $publicKey, ['binary' => true]); - Note that the length of the concatenation of 'packet_length', - 'padding_length', 'payload', and 'random padding' MUST be a multiple - of the cipher block size or 8, whichever is larger. - */ - $paddingLength = (7 * strlen($paddedKey)) % 8; - for ($i = 1; $i <= $paddingLength; $i++) { - $paddedKey.= chr($i); - } - $key = Strings::packSSH2('sssNss', 'none', 'none', '', 1, $publicKey, $paddedKey); - $key = "openssh-key-v1\0$key"; + $privateKey = Strings::packSSH2('sssi', 'ecdsa-sha2-' . $alias, $alias, $points, $privateKey); - return "-----BEGIN OPENSSH PRIVATE KEY-----\r\n" . - chunk_split(Base64::encode($key), 70) . - "-----END OPENSSH PRIVATE KEY-----"; + return self::wrapPrivateKey($publicKey, $privateKey, $options); } } diff --git a/phpseclib/Crypt/RSA/Keys/OpenSSH.php b/phpseclib/Crypt/RSA/Keys/OpenSSH.php index 6d93e5bb..360ec98d 100644 --- a/phpseclib/Crypt/RSA/Keys/OpenSSH.php +++ b/phpseclib/Crypt/RSA/Keys/OpenSSH.php @@ -31,6 +31,13 @@ use phpseclib\Crypt\Common\Keys\OpenSSH as Progenitor; */ abstract class OpenSSH extends Progenitor { + /** + * Supported Key Types + * + * @var array + */ + protected static $types = ['ssh-rsa']; + /** * Break a public or private key down into its constituent components * @@ -41,19 +48,48 @@ abstract class OpenSSH extends Progenitor */ public static function load($key, $password = '') { - $key = parent::load($key, 'ssh-rsa'); - - $result = Strings::unpackSSH2('ii', $key); - if ($result === false) { - throw new \UnexpectedValueException('Key appears to be malformed'); + static $one; + if (!isset($one)) { + $one = new BigInteger(1); } - list($publicExponent, $modulus) = $result; + + $parsed = parent::load($key, $password); + + if (isset($parsed['paddedKey'])) { + list($type) = Strings::unpackSSH2('s', $parsed['paddedKey']); + if ($type != $parsed['type']) { + throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])"); + } + + $primes = $coefficients = []; + + list( + $modulus, + $publicExponent, + $privateExponent, + $coefficients[2], + $primes[1], + $primes[2], + $comment, + ) = Strings::unpackSSH2('i6s', $parsed['paddedKey']); + + $temp = $primes[1]->subtract($one); + $exponents = [1 => $publicExponent->modInverse($temp)]; + $temp = $primes[2]->subtract($one); + $exponents[] = $publicExponent->modInverse($temp); + + $isPublicKey = false; + + return compact('publicExponent', 'modulus', 'privateExponent', 'primes', 'coefficients', 'exponents', 'comment', 'isPublicKey'); + } + + list($publicExponent, $modulus) = Strings::unpackSSH2('ii', $parsed['publicKey']); return [ 'isPublicKey' => true, 'modulus' => $modulus, 'publicExponent' => $publicExponent, - 'comment' => parent::getComment($key) + 'comment' => $parsed['comment'] ]; } @@ -75,8 +111,30 @@ abstract class OpenSSH extends Progenitor } $comment = isset($options['comment']) ? $options['comment'] : self::$comment; - $RSAPublicKey = 'ssh-rsa ' . Base64::encode($RSAPublicKey) . ' ' . $comment; + $RSAPublicKey = 'ssh-rsa ' . base64_encode($RSAPublicKey) . ' ' . $comment; return $RSAPublicKey; } + + /** + * 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, array $primes, array $exponents, array $coefficients, $password = '', array $options = []) + { + $publicKey = self::savePublicKey($n, $e, ['binary' => true]); + $privateKey = Strings::packSSH2('si6', 'ssh-rsa', $n, $e, $d, $coefficients[2], $primes[1], $primes[2]); + + return self::wrapPrivateKey($publicKey, $privateKey, $options); + } } diff --git a/tests/Unit/Crypt/DSA/LoadKeyTest.php b/tests/Unit/Crypt/DSA/LoadKeyTest.php index 7397317e..0e078a8e 100644 --- a/tests/Unit/Crypt/DSA/LoadKeyTest.php +++ b/tests/Unit/Crypt/DSA/LoadKeyTest.php @@ -218,4 +218,41 @@ ZpmyOpXM/0opRMIRdmqVW4ardBFNokmlqngwcbaptfRnk9W2cQtx0lmKy6X/vnis strtolower(preg_replace('#\s#', '', $key)) ); } + + public function testOpenSSHPrivate() + { + $key = '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABswAAAAdzc2gtZH +NzAAAAgQDpE1/71V6uuaeEqbaAzoEsA1kdJBZh9In3/VlCXwvlJ6zz8KSzbQxrC45sO7y9 +fMwD5QyWEphVeIXO/NSfcZhK/SD/D+N1Zx52Ku2KEFTb3dAhfNGe9yhsrAVI5WyE4lS2qe +e5fLNnh138hYAdN7ENRoUAQ3I6Hk9HAIn+ltHMmQAAABUA95iPdxHL3ikkmZd1X5WhQFTI ++9sAAACBAMcn1PdWdUmE8D4KP6g0rq4KAElZc904mYX+bHQNMXONm4BrsScn3/iOf370Ea +iUgkomo+CSP2H8S3pLBNbiQW7AzS9TGT782FlG/bXf8kSMFb7IzAuFmQMeouLZo40AwHEv +7PpdzrXs6GRQ0vwJlNoqoUAUi9MMhexDzpGMbNjqAAAAgQCU1JuJZDzpk+cBgEdRTRGx6m +JZkP9vHP7ctUhgKZcAPSyd8keN8gQCpvmZuK1ADtd/+pXBxbQBAPb1+p8wAgqDU4m8+LFf +2igKtb8mf8qp/ghxV08/Tzf5WfcDWPxOesdlN48qLbSmUgsO7gq/1vodebMSHcduV4JTq8 +ix5Ey87QAAAeiOLNHLjizRywAAAAdzc2gtZHNzAAAAgQDpE1/71V6uuaeEqbaAzoEsA1kd +JBZh9In3/VlCXwvlJ6zz8KSzbQxrC45sO7y9fMwD5QyWEphVeIXO/NSfcZhK/SD/D+N1Zx +52Ku2KEFTb3dAhfNGe9yhsrAVI5WyE4lS2qee5fLNnh138hYAdN7ENRoUAQ3I6Hk9HAIn+ +ltHMmQAAABUA95iPdxHL3ikkmZd1X5WhQFTI+9sAAACBAMcn1PdWdUmE8D4KP6g0rq4KAE +lZc904mYX+bHQNMXONm4BrsScn3/iOf370EaiUgkomo+CSP2H8S3pLBNbiQW7AzS9TGT78 +2FlG/bXf8kSMFb7IzAuFmQMeouLZo40AwHEv7PpdzrXs6GRQ0vwJlNoqoUAUi9MMhexDzp +GMbNjqAAAAgQCU1JuJZDzpk+cBgEdRTRGx6mJZkP9vHP7ctUhgKZcAPSyd8keN8gQCpvmZ +uK1ADtd/+pXBxbQBAPb1+p8wAgqDU4m8+LFf2igKtb8mf8qp/ghxV08/Tzf5WfcDWPxOes +dlN48qLbSmUgsO7gq/1vodebMSHcduV4JTq8ix5Ey87QAAABQhHEzWiduF4V0DestSnJ3q +9GNNTQAAAAxyb290QHZhZ3JhbnQBAgMEBQ== +-----END OPENSSH PRIVATE KEY-----'; + + $key = PublicKeyLoader::load($key); + + $key2 = PublicKeyLoader::load($key->toString('OpenSSH')); + $this->assertInstanceOf(PrivateKey::class, $key2); + + $sig = $key->sign('zzz'); + + $key = 'ssh-dss AAAAB3NzaC1kc3MAAACBAOkTX/vVXq65p4SptoDOgSwDWR0kFmH0iff9WUJfC+UnrPPwpLNtDGsLjmw7vL18zAPlDJYSmFV4hc781J9xmEr9IP8P43VnHnYq7YoQVNvd0CF80Z73KGysBUjlbITiVLap57l8s2eHXfyFgB03sQ1GhQBDcjoeT0cAif6W0cyZAAAAFQD3mI93EcveKSSZl3VflaFAVMj72wAAAIEAxyfU91Z1SYTwPgo/qDSurgoASVlz3TiZhf5sdA0xc42bgGuxJyff+I5/fvQRqJSCSiaj4JI/YfxLeksE1uJBbsDNL1MZPvzYWUb9td/yRIwVvsjMC4WZAx6i4tmjjQDAcS/s+l3OtezoZFDS/AmU2iqhQBSL0wyF7EPOkYxs2OoAAACBAJTUm4lkPOmT5wGAR1FNEbHqYlmQ/28c/ty1SGAplwA9LJ3yR43yBAKm+Zm4rUAO13/6lcHFtAEA9vX6nzACCoNTibz4sV/aKAq1vyZ/yqn+CHFXTz9PN/lZ9wNY/E56x2U3jyottKZSCw7uCr/W+h15sxIdx25XglOryLHkTLzt root@vagrant'; + $key = PublicKeyLoader::load($key); + + $this->assertTrue($key->verify('zzz', $sig)); + } } diff --git a/tests/Unit/Crypt/ECDSA/KeyTest.php b/tests/Unit/Crypt/ECDSA/KeyTest.php index 6149a041..353e52ea 100644 --- a/tests/Unit/Crypt/ECDSA/KeyTest.php +++ b/tests/Unit/Crypt/ECDSA/KeyTest.php @@ -12,6 +12,7 @@ use phpseclib\Crypt\ECDSA\Keys\PuTTY; use phpseclib\Crypt\ECDSA\Keys\OpenSSH; use phpseclib\Crypt\ECDSA\Keys\XML; use phpseclib\Crypt\PublicKeyLoader; +use phpseclib\Crypt\ECDSA\PrivateKey; class Unit_Crypt_ECDSA_LoadKeyTest extends PhpseclibTestCase { @@ -441,4 +442,48 @@ pomV7r6gmoMYteGVABfgAAAAD3ZhZ3JhbnRAdmFncmFudAECAwQFBg== $actual = str_replace("\r\n", "\n", $actual); return parent::assertSame($expected, $actual, $message); } + + public function testOpenSSHPrivateECDSA() + { + $key = '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS +1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTk2tbDiyQPzljR+LLIsMzJiwqkfHkG +StUt3kO00FKMoYv3RJfP6mqdE3E3pPcT5cBg4yB+KzYsYDxwuBc03oQcAAAAqCTU2l0k1N +pdAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOTa1sOLJA/OWNH4 +ssiwzMmLCqR8eQZK1S3eQ7TQUoyhi/dEl8/qap0TcTek9xPlwGDjIH4rNixgPHC4FzTehB +wAAAAgZ8mK8+EsQ46susQn4mwMNmpvTaKX9Q9KDvOrzotP2qgAAAAMcm9vdEB2YWdyYW50 +AQIDBA== +-----END OPENSSH PRIVATE KEY-----'; + + $key = PublicKeyLoader::load($key); + + $key2 = PublicKeyLoader::load($key->toString('OpenSSH')); + $this->assertInstanceOf(PrivateKey::class, $key2); + + $sig = $key->sign('zzz'); + + $key = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOTa1sOLJA/OWNH4ssiwzMmLCqR8eQZK1S3eQ7TQUoyhi/dEl8/qap0TcTek9xPlwGDjIH4rNixgPHC4FzTehBw= root@vagrant'; + $key = PublicKeyLoader::load($key); + + $this->assertTrue($key->verify('zzz', $sig)); + } + + public function testOpenSSHPrivateEd25519() + { + $key = '-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACChhCZwqkIh43AfURPOgbyYeZRCKvd4jFcyAK4xmiqxQwAAAJDqGgwS6hoM +EgAAAAtzc2gtZWQyNTUxOQAAACChhCZwqkIh43AfURPOgbyYeZRCKvd4jFcyAK4xmiqxQw +AAAEDzL/Yl1Vr/5MxhIIEkVKXBMEIumVG8gUjT9i2PTGSehqGEJnCqQiHjcB9RE86BvJh5 +lEIq93iMVzIArjGaKrFDAAAADHJvb3RAdmFncmFudAE= +-----END OPENSSH PRIVATE KEY-----'; + + $key = PublicKeyLoader::load($key); + $sig = $key->sign('zzz'); + + $key = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKGEJnCqQiHjcB9RE86BvJh5lEIq93iMVzIArjGaKrFD root@vagrant'; + $key = PublicKeyLoader::load($key); + + $this->assertTrue($key->verify('zzz', $sig)); + } } diff --git a/tests/Unit/Crypt/RSA/LoadKeyTest.php b/tests/Unit/Crypt/RSA/LoadKeyTest.php index 66cb5ff6..d11772e1 100644 --- a/tests/Unit/Crypt/RSA/LoadKeyTest.php +++ b/tests/Unit/Crypt/RSA/LoadKeyTest.php @@ -925,4 +925,58 @@ IBgv3a3Lyb+IQtT75LE1yjE= $this->assertSame($r['MGFHash'], $r2['MGFHash']); $this->assertSame($r['saltLength'], $r2['saltLength']); } + + public function testOpenSSHPrivate() + { + $key = '-----BEGIN OPENSSH PRIVATE KEY----- +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 +-----END OPENSSH PRIVATE KEY-----'; + + $key = PublicKeyLoader::load($key); + + $key2 = PublicKeyLoader::load($key->toString('OpenSSH')); + $this->assertInstanceOf(PrivateKey::class, $key2); + + $sig = $key->sign('zzz'); + + $key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDS8/TfgDLaowESNlVxYcLOSEPS1SAeBTIu5S2s8oRlTTrBD9ZyGSJBdsPR2ADGVmhD9lHQ3dbhjSpkIqC4msZ+2yPLJyIpyOY9BdW+V4sUfQUO5BOO8koyIThDSkJYdpsP2SWUtZU5gn+LAFqcGlSfj90BBoYWMtCNEsgBFMjo1kYDGYhoFv5wjyxz6rMHRZ9swYzT7wA7nJ5pmdEm10YCH0AcM4dP5Xqu5t1hWyhgEgd2UCfNy/4RJSFBhgk7Jb+rah3ovwgXn5ZU9CLPyc17fOeNFyT5P8pJjpfx9L9XJ+ZAOOSZIxSTePe4HrWo3NR1E2ZMqBNOsx8ljlsz6MNxShwfk6leFg40nh2k/VAOoWB7j5loKfkzPz0GOGQlR44xaVoHwqq/PsHOKUKYP6q/hOiyxOpxkgayB0QgbsdK32VWzPSZ523/r35iaS6GKRJ9nREAA74lRnmBdMbAWbQEWklY0vEl46yADpZCZRAZEretjXCDYsw0ri7RXKj8u8s= root@vagrant'; + $key = PublicKeyLoader::load($key); + + $this->assertTrue($key->verify('zzz', $sig)); + } }