add expanded support for OpenSSH private keys

This commit is contained in:
terrafrost 2019-06-08 06:22:57 -05:00
parent 88b6337a3f
commit 327f555b7c
8 changed files with 442 additions and 190 deletions

View File

@ -19,6 +19,7 @@ namespace phpseclib\Crypt\Common\Keys;
use ParagonIE\ConstantTime\Base64; use ParagonIE\ConstantTime\Base64;
use phpseclib\Common\Functions\Strings; use phpseclib\Common\Functions\Strings;
use phpseclib\Crypt\Random;
/** /**
* OpenSSH Formatted RSA Key Handler * OpenSSH Formatted RSA Key Handler
@ -66,50 +67,99 @@ abstract class OpenSSH
* @param string $type * @param string $type
* @return array * @return array
*/ */
public static function load($key, $type) public static function load($key, $password = '')
{ {
if (!is_string($key)) { if (!is_string($key)) {
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($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); $parts = explode(' ', $key, 3);
if (!isset($parts[1])) { if (!isset($parts[1])) {
$key = Base64::decode($parts[0]); $key = base64_decode($parts[0]);
$comment = isset($parts[1]) ? $parts[1] : false; $comment = isset($parts[1]) ? $parts[1] : false;
} else { } else {
if ($parts[0] != $type) { $asciiType = $parts[0];
throw new \UnexpectedValueException('Expected a ' . $type . ' key - got a ' . $parts[0] . ' key'); self::checkType($parts[0]);
} $key = base64_decode($parts[1]);
$key = Base64::decode($parts[1]);
$comment = isset($parts[2]) ? $parts[2] : false; $comment = isset($parts[2]) ? $parts[2] : false;
} }
if ($key === false) { if ($key === false) {
throw new \UnexpectedValueException('Key should be a string - not a ' . gettype($key)); 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) { list($type) = Strings::unpackSSH2('s', $key);
throw new \UnexpectedValueException('Key appears to be malformed'); 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) { if (strlen($key) <= 4) {
throw new \UnexpectedValueException('Key appears to be malformed'); throw new \UnexpectedValueException('Key appears to be malformed');
} }
return $key; $publicKey = $key;
}
/** return compact('type', 'publicKey', 'comment');
* 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;
} }
/** /**
@ -125,4 +175,53 @@ abstract class OpenSSH
{ {
self::$binary = $enabled; 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-----";
}
} }

View File

@ -31,6 +31,13 @@ use phpseclib\Crypt\Common\Keys\OpenSSH as Progenitor;
*/ */
abstract class OpenSSH extends 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 * 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 = '') public static function load($key, $password = '')
{ {
$key = parent::load($key, 'ssh-dss'); $parsed = parent::load($key, 'ssh-dss');
$result = Strings::unpackSSH2('iiii', $key); if (isset($parsed['paddedKey'])) {
if ($result === false) { list($type) = Strings::unpackSSH2('s', $parsed['paddedKey']);
throw new \UnexpectedValueException('Key appears to be malformed'); 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) = $result;
$comment = parent::getComment($key); 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) = Strings::unpackSSH2('iiii', $parsed['publicKey']);
$comment = $parsed['comment'];
return compact('p', 'q', 'g', 'y', '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; $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
$DSAPublicKey = 'ssh-dss ' . Base64::encode($DSAPublicKey) . ' ' . $comment; $DSAPublicKey = 'ssh-dss ' . base64_encode($DSAPublicKey) . ' ' . $comment;
return $DSAPublicKey; 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);
}
} }

View File

@ -27,7 +27,7 @@ use phpseclib\Crypt\Common\Keys\PuTTY as Progenitor;
/** /**
* PuTTY Formatted DSA Key Handler * PuTTY Formatted DSA Key Handler
* *
* @package RSA * @package DSA
* @author Jim Wigginton <terrafrost@php.net> * @author Jim Wigginton <terrafrost@php.net>
* @access public * @access public
*/ */
@ -66,17 +66,8 @@ abstract class PuTTY extends Progenitor
extract($components); extract($components);
unset($components['public'], $components['private']); unset($components['public'], $components['private']);
$result = Strings::unpackSSH2('iiii', $public); list($p, $q, $g, $y) = Strings::unpackSSH2('iiii', $public);
if ($result === false) { list($x) = Strings::unpackSSH2('i', $private);
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;
return compact('p', 'q', 'g', 'y', 'x', 'comment'); return compact('p', 'q', 'g', 'y', 'x', 'comment');
} }

View File

@ -25,7 +25,6 @@ use phpseclib\Crypt\ECDSA\BaseCurves\Base as BaseCurve;
use phpseclib\Exception\UnsupportedCurveException; use phpseclib\Exception\UnsupportedCurveException;
use phpseclib\Crypt\ECDSA\Curves\Ed25519; use phpseclib\Crypt\ECDSA\Curves\Ed25519;
use phpseclib\Math\Common\FiniteField\Integer; use phpseclib\Math\Common\FiniteField\Integer;
use phpseclib\Crypt\Random;
/** /**
* OpenSSH Formatted ECDSA Key Handler * OpenSSH Formatted ECDSA Key Handler
@ -43,7 +42,7 @@ abstract class OpenSSH extends Progenitor
* *
* @var array * @var array
*/ */
private static $types = [ protected static $types = [
'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp384',
'ecdsa-sha2-nistp521', 'ecdsa-sha2-nistp521',
@ -60,113 +59,39 @@ abstract class OpenSSH extends Progenitor
*/ */
public static function load($key, $password = '') public static function load($key, $password = '')
{ {
/* $parsed = parent::load($key, $password);
key format is described here:
https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
this is only supported for ECDSA because of Ed25519. ssh-keygen doesn't generate a if (isset($parsed['paddedKey'])) {
PKCS1/8 formatted private key for Ed25519 - it generates an OpenSSH formatted $paddedKey = $parsed['paddedKey'];
private key. probably because, at the time of this writing, there's not an actual list($type) = Strings::unpackSSH2('s', $paddedKey);
IETF RFC describing an Ed25519 format if ($type != $parsed['type']) {
*/ throw new \RuntimeException("The public and private keys are not of the same type ($type vs $parsed[type])");
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 ($type == 'ssh-ed25519' ) {
if ($numKeys != 1) { list(, $key, $comment) = Strings::unpackSSH2('sss', $paddedKey);
throw new \RuntimeException('Although the OpenSSH private key format supports multiple keys phpseclib does not'); $key = libsodium::load($key);
$key['comment'] = $comment;
return $key;
} }
if (strlen($kdfoptions) || $kdfname != 'none' || $ciphername != 'none') { list($curveName, $publicKey, $privateKey, $comment) = Strings::unpackSSH2('ssis', $paddedKey);
/* $curve = self::loadCurveByParam(['namedCurve' => $curveName]);
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();
return [ return [
'curve' => $curve, 'curve' => $curve,
'dA' => $curve->extractSecret($privateKey), 'dA' => $curve->convertInteger($privateKey),
'QA' => self::extractPoint($publicKey, $curve), 'QA' => self::extractPoint("\0$publicKey", $curve),
'comment' => $comment 'comment' => $comment
]; ];
} }
$parts = explode(' ', $key, 3); if ($parsed['type'] == 'ssh-ed25519') {
if (Strings::shift($parsed['publicKey'], 4) != "\0\0\0\x20") {
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") {
throw new \RuntimeException('Length of ssh-ed25519 key should be 32'); throw new \RuntimeException('Length of ssh-ed25519 key should be 32');
} }
$curve = new Ed25519(); $curve = new Ed25519();
$qa = self::extractPoint($key, $curve); $qa = self::extractPoint($parsed['publicKey'], $curve);
} else { } else {
list($curveName, $publicKey) = Strings::unpackSSH2('ss', $key); list($curveName, $publicKey) = Strings::unpackSSH2('ss', $parsed['publicKey']);
$curveName = '\phpseclib\Crypt\ECDSA\Curves\\' . $curveName; $curveName = '\phpseclib\Crypt\ECDSA\Curves\\' . $curveName;
$curve = new $curveName(); $curve = new $curveName();
@ -176,34 +101,17 @@ abstract class OpenSSH extends Progenitor
return [ return [
'curve' => $curve, 'curve' => $curve,
'QA' => $qa, '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 * @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(); self::initialize_static_variables();
$reflect = new \ReflectionClass($curve); $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'); 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(); $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
$key = Strings::packSSH2('sss', 'ecdsa-sha2-' . $alias, $alias, $points); $key = Strings::packSSH2('sss', 'ecdsa-sha2-' . $alias, $alias, $points);
@ -233,7 +170,7 @@ abstract class OpenSSH extends Progenitor
return $key; return $key;
} }
$key = 'ecdsa-sha2-' . $alias . ' ' . Base64::encode($key) . ' ' . $comment; $key = 'ecdsa-sha2-' . $alias . ' ' . base64_encode($key) . ' ' . $comment;
return $key; return $key;
} }
@ -246,10 +183,12 @@ abstract class OpenSSH extends Progenitor
* @param \phpseclib\Crypt\ECDSA\Curves\Ed25519 $curve * @param \phpseclib\Crypt\ECDSA\Curves\Ed25519 $curve
* @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey * @param \phpseclib\Math\Common\FiniteField\Integer[] $publicKey
* @param string $password optional * @param string $password optional
* @param array $options optional
* @return string * @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 ($curve instanceof Ed25519) {
if (!isset($privateKey->secret)) { if (!isset($privateKey->secret)) {
throw new \RuntimeException('Private Key does not have a secret set'); throw new \RuntimeException('Private Key does not have a secret set');
} }
@ -257,27 +196,21 @@ abstract class OpenSSH extends Progenitor
throw new \RuntimeException('Private Key secret is not of the correct length'); throw new \RuntimeException('Private Key secret is not of the correct length');
} }
list(, $checkint) = unpack('N', Random::string(4));
$pubKey = $curve->encodePoint($publicKey); $pubKey = $curve->encodePoint($publicKey);
$publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey); $publicKey = Strings::packSSH2('ss', 'ssh-ed25519', $pubKey);
$paddedKey = Strings::packSSH2('NNssss', $checkint, $checkint, 'ssh-ed25519', $pubKey, $privateKey->secret . $pubKey, self::$comment); $privateKey = Strings::packSSH2('sss', 'ssh-ed25519', $pubKey, $privateKey->secret . $pubKey);
/*
from http://tools.ietf.org/html/rfc4253#section-6 :
Note that the length of the concatenation of 'packet_length', return self::wrapPrivateKey($publicKey, $privateKey, $options);
'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" . $alias = self::getAlias($curve);
chunk_split(Base64::encode($key), 70) .
"-----END OPENSSH PRIVATE KEY-----"; $points = "\4" . $publicKey[0]->toBytes() . $publicKey[1]->toBytes();
$publicKey = self::savePublicKey($curve, $publicKey, ['binary' => true]);
$privateKey = Strings::packSSH2('sssi', 'ecdsa-sha2-' . $alias, $alias, $points, $privateKey);
return self::wrapPrivateKey($publicKey, $privateKey, $options);
} }
} }

View File

@ -31,6 +31,13 @@ use phpseclib\Crypt\Common\Keys\OpenSSH as Progenitor;
*/ */
abstract class OpenSSH extends 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 * 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 = '') public static function load($key, $password = '')
{ {
$key = parent::load($key, 'ssh-rsa'); static $one;
if (!isset($one)) {
$result = Strings::unpackSSH2('ii', $key); $one = new BigInteger(1);
if ($result === false) {
throw new \UnexpectedValueException('Key appears to be malformed');
} }
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 [ return [
'isPublicKey' => true, 'isPublicKey' => true,
'modulus' => $modulus, 'modulus' => $modulus,
'publicExponent' => $publicExponent, '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; $comment = isset($options['comment']) ? $options['comment'] : self::$comment;
$RSAPublicKey = 'ssh-rsa ' . Base64::encode($RSAPublicKey) . ' ' . $comment; $RSAPublicKey = 'ssh-rsa ' . base64_encode($RSAPublicKey) . ' ' . $comment;
return $RSAPublicKey; 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);
}
} }

View File

@ -218,4 +218,41 @@ ZpmyOpXM/0opRMIRdmqVW4ardBFNokmlqngwcbaptfRnk9W2cQtx0lmKy6X/vnis
strtolower(preg_replace('#\s#', '', $key)) 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));
}
} }

View File

@ -12,6 +12,7 @@ use phpseclib\Crypt\ECDSA\Keys\PuTTY;
use phpseclib\Crypt\ECDSA\Keys\OpenSSH; use phpseclib\Crypt\ECDSA\Keys\OpenSSH;
use phpseclib\Crypt\ECDSA\Keys\XML; use phpseclib\Crypt\ECDSA\Keys\XML;
use phpseclib\Crypt\PublicKeyLoader; use phpseclib\Crypt\PublicKeyLoader;
use phpseclib\Crypt\ECDSA\PrivateKey;
class Unit_Crypt_ECDSA_LoadKeyTest extends PhpseclibTestCase class Unit_Crypt_ECDSA_LoadKeyTest extends PhpseclibTestCase
{ {
@ -441,4 +442,48 @@ pomV7r6gmoMYteGVABfgAAAAD3ZhZ3JhbnRAdmFncmFudAECAwQFBg==
$actual = str_replace("\r\n", "\n", $actual); $actual = str_replace("\r\n", "\n", $actual);
return parent::assertSame($expected, $actual, $message); 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));
}
} }

View File

@ -925,4 +925,58 @@ IBgv3a3Lyb+IQtT75LE1yjE=
$this->assertSame($r['MGFHash'], $r2['MGFHash']); $this->assertSame($r['MGFHash'], $r2['MGFHash']);
$this->assertSame($r['saltLength'], $r2['saltLength']); $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));
}
} }