Skip to content

Commit d359caa

Browse files
marainogopherbot
authored andcommitted
ssh: support for marshaling keys using the OpenSSH format
This adds methods to marshal private keys, encrypted and unencrypted to the OpenSSH format. Fixes golang/go#37132 Change-Id: I1a95301f789ce04858e6b147748c6e8b7700384b Reviewed-on: https://go-review.googlesource.com/c/crypto/+/218620 Run-TryBot: Roland Shoemaker <[email protected]> TryBot-Result: Gopher Robot <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> Auto-Submit: Roland Shoemaker <[email protected]>
1 parent c5370d2 commit d359caa

File tree

2 files changed

+308
-46
lines changed

2 files changed

+308
-46
lines changed

ssh/keys.go

Lines changed: 240 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import (
1313
"crypto/ecdsa"
1414
"crypto/elliptic"
1515
"crypto/md5"
16+
"crypto/rand"
1617
"crypto/rsa"
1718
"crypto/sha256"
1819
"crypto/x509"
1920
"encoding/asn1"
2021
"encoding/base64"
22+
"encoding/binary"
2123
"encoding/hex"
2224
"encoding/pem"
2325
"errors"
@@ -295,6 +297,18 @@ func MarshalAuthorizedKey(key PublicKey) []byte {
295297
return b.Bytes()
296298
}
297299

300+
// MarshalPrivateKey returns a PEM block with the private key serialized in the
301+
// OpenSSH format.
302+
func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) {
303+
return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaler)
304+
}
305+
306+
// MarshalPrivateKeyWithPassphrase returns a PEM block holding the encrypted
307+
// private key serialized in the OpenSSH format.
308+
func MarshalPrivateKeyWithPassphrase(key crypto.PrivateKey, comment string, passphrase []byte) (*pem.Block, error) {
309+
return marshalOpenSSHPrivateKey(key, comment, passphraseProtectedOpenSSHMarshaler(passphrase))
310+
}
311+
298312
// PublicKey represents a public key using an unspecified algorithm.
299313
//
300314
// Some PublicKeys provided by this package also implement CryptoPublicKey.
@@ -1241,28 +1255,106 @@ func passphraseProtectedOpenSSHKey(passphrase []byte) openSSHDecryptFunc {
12411255
}
12421256
}
12431257

1258+
func unencryptedOpenSSHMarshaler(privKeyBlock []byte) ([]byte, string, string, string, error) {
1259+
key := generateOpenSSHPadding(privKeyBlock, 8)
1260+
return key, "none", "none", "", nil
1261+
}
1262+
1263+
func passphraseProtectedOpenSSHMarshaler(passphrase []byte) openSSHEncryptFunc {
1264+
return func(privKeyBlock []byte) ([]byte, string, string, string, error) {
1265+
salt := make([]byte, 16)
1266+
if _, err := rand.Read(salt); err != nil {
1267+
return nil, "", "", "", err
1268+
}
1269+
1270+
opts := struct {
1271+
Salt []byte
1272+
Rounds uint32
1273+
}{salt, 16}
1274+
1275+
// Derive key to encrypt the private key block.
1276+
k, err := bcrypt_pbkdf.Key(passphrase, salt, int(opts.Rounds), 32+aes.BlockSize)
1277+
if err != nil {
1278+
return nil, "", "", "", err
1279+
}
1280+
1281+
// Add padding matching the block size of AES.
1282+
keyBlock := generateOpenSSHPadding(privKeyBlock, aes.BlockSize)
1283+
1284+
// Encrypt the private key using the derived secret.
1285+
1286+
dst := make([]byte, len(keyBlock))
1287+
key, iv := k[:32], k[32:]
1288+
block, err := aes.NewCipher(key)
1289+
if err != nil {
1290+
return nil, "", "", "", err
1291+
}
1292+
1293+
stream := cipher.NewCTR(block, iv)
1294+
stream.XORKeyStream(dst, keyBlock)
1295+
1296+
return dst, "aes256-ctr", "bcrypt", string(Marshal(opts)), nil
1297+
}
1298+
}
1299+
1300+
const privateKeyAuthMagic = "openssh-key-v1\x00"
1301+
12441302
type openSSHDecryptFunc func(CipherName, KdfName, KdfOpts string, PrivKeyBlock []byte) ([]byte, error)
1303+
type openSSHEncryptFunc func(PrivKeyBlock []byte) (ProtectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error)
1304+
1305+
type openSSHEncryptedPrivateKey struct {
1306+
CipherName string
1307+
KdfName string
1308+
KdfOpts string
1309+
NumKeys uint32
1310+
PubKey []byte
1311+
PrivKeyBlock []byte
1312+
}
1313+
1314+
type openSSHPrivateKey struct {
1315+
Check1 uint32
1316+
Check2 uint32
1317+
Keytype string
1318+
Rest []byte `ssh:"rest"`
1319+
}
1320+
1321+
type openSSHRSAPrivateKey struct {
1322+
N *big.Int
1323+
E *big.Int
1324+
D *big.Int
1325+
Iqmp *big.Int
1326+
P *big.Int
1327+
Q *big.Int
1328+
Comment string
1329+
Pad []byte `ssh:"rest"`
1330+
}
1331+
1332+
type openSSHEd25519PrivateKey struct {
1333+
Pub []byte
1334+
Priv []byte
1335+
Comment string
1336+
Pad []byte `ssh:"rest"`
1337+
}
1338+
1339+
type openSSHECDSAPrivateKey struct {
1340+
Curve string
1341+
Pub []byte
1342+
D *big.Int
1343+
Comment string
1344+
Pad []byte `ssh:"rest"`
1345+
}
12451346

12461347
// parseOpenSSHPrivateKey parses an OpenSSH private key, using the decrypt
12471348
// function to unwrap the encrypted portion. unencryptedOpenSSHKey can be used
12481349
// as the decrypt function to parse an unencrypted private key. See
12491350
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
12501351
func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.PrivateKey, error) {
1251-
const magic = "openssh-key-v1\x00"
1252-
if len(key) < len(magic) || string(key[:len(magic)]) != magic {
1352+
if len(key) < len(privateKeyAuthMagic) || string(key[:len(privateKeyAuthMagic)]) != privateKeyAuthMagic {
12531353
return nil, errors.New("ssh: invalid openssh private key format")
12541354
}
1255-
remaining := key[len(magic):]
1256-
1257-
var w struct {
1258-
CipherName string
1259-
KdfName string
1260-
KdfOpts string
1261-
NumKeys uint32
1262-
PubKey []byte
1263-
PrivKeyBlock []byte
1264-
}
1355+
remaining := key[len(privateKeyAuthMagic):]
12651356

1357+
var w openSSHEncryptedPrivateKey
12661358
if err := Unmarshal(remaining, &w); err != nil {
12671359
return nil, err
12681360
}
@@ -1284,13 +1376,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
12841376
return nil, err
12851377
}
12861378

1287-
pk1 := struct {
1288-
Check1 uint32
1289-
Check2 uint32
1290-
Keytype string
1291-
Rest []byte `ssh:"rest"`
1292-
}{}
1293-
1379+
var pk1 openSSHPrivateKey
12941380
if err := Unmarshal(privKeyBlock, &pk1); err != nil || pk1.Check1 != pk1.Check2 {
12951381
if w.CipherName != "none" {
12961382
return nil, x509.IncorrectPasswordError
@@ -1300,18 +1386,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
13001386

13011387
switch pk1.Keytype {
13021388
case KeyAlgoRSA:
1303-
// https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L2760-L2773
1304-
key := struct {
1305-
N *big.Int
1306-
E *big.Int
1307-
D *big.Int
1308-
Iqmp *big.Int
1309-
P *big.Int
1310-
Q *big.Int
1311-
Comment string
1312-
Pad []byte `ssh:"rest"`
1313-
}{}
1314-
1389+
var key openSSHRSAPrivateKey
13151390
if err := Unmarshal(pk1.Rest, &key); err != nil {
13161391
return nil, err
13171392
}
@@ -1337,13 +1412,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
13371412

13381413
return pk, nil
13391414
case KeyAlgoED25519:
1340-
key := struct {
1341-
Pub []byte
1342-
Priv []byte
1343-
Comment string
1344-
Pad []byte `ssh:"rest"`
1345-
}{}
1346-
1415+
var key openSSHEd25519PrivateKey
13471416
if err := Unmarshal(pk1.Rest, &key); err != nil {
13481417
return nil, err
13491418
}
@@ -1360,14 +1429,7 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
13601429
copy(pk, key.Priv)
13611430
return &pk, nil
13621431
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
1363-
key := struct {
1364-
Curve string
1365-
Pub []byte
1366-
D *big.Int
1367-
Comment string
1368-
Pad []byte `ssh:"rest"`
1369-
}{}
1370-
1432+
var key openSSHECDSAPrivateKey
13711433
if err := Unmarshal(pk1.Rest, &key); err != nil {
13721434
return nil, err
13731435
}
@@ -1415,6 +1477,131 @@ func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.Priv
14151477
}
14161478
}
14171479

1480+
func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, encrypt openSSHEncryptFunc) (*pem.Block, error) {
1481+
var w openSSHEncryptedPrivateKey
1482+
var pk1 openSSHPrivateKey
1483+
1484+
// Random check bytes.
1485+
var check uint32
1486+
if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
1487+
return nil, err
1488+
}
1489+
1490+
pk1.Check1 = check
1491+
pk1.Check2 = check
1492+
w.NumKeys = 1
1493+
1494+
// Use a []byte directly on ed25519 keys.
1495+
if k, ok := key.(*ed25519.PrivateKey); ok {
1496+
key = *k
1497+
}
1498+
1499+
switch k := key.(type) {
1500+
case *rsa.PrivateKey:
1501+
E := new(big.Int).SetInt64(int64(k.PublicKey.E))
1502+
// Marshal public key:
1503+
// E and N are in reversed order in the public and private key.
1504+
pubKey := struct {
1505+
KeyType string
1506+
E *big.Int
1507+
N *big.Int
1508+
}{
1509+
KeyAlgoRSA,
1510+
E, k.PublicKey.N,
1511+
}
1512+
w.PubKey = Marshal(pubKey)
1513+
1514+
// Marshal private key.
1515+
key := openSSHRSAPrivateKey{
1516+
N: k.PublicKey.N,
1517+
E: E,
1518+
D: k.D,
1519+
Iqmp: k.Precomputed.Qinv,
1520+
P: k.Primes[0],
1521+
Q: k.Primes[1],
1522+
Comment: comment,
1523+
}
1524+
pk1.Keytype = KeyAlgoRSA
1525+
pk1.Rest = Marshal(key)
1526+
case ed25519.PrivateKey:
1527+
pub := make([]byte, ed25519.PublicKeySize)
1528+
priv := make([]byte, ed25519.PrivateKeySize)
1529+
copy(pub, k[32:])
1530+
copy(priv, k)
1531+
1532+
// Marshal public key.
1533+
pubKey := struct {
1534+
KeyType string
1535+
Pub []byte
1536+
}{
1537+
KeyAlgoED25519, pub,
1538+
}
1539+
w.PubKey = Marshal(pubKey)
1540+
1541+
// Marshal private key.
1542+
key := openSSHEd25519PrivateKey{
1543+
Pub: pub,
1544+
Priv: priv,
1545+
Comment: comment,
1546+
}
1547+
pk1.Keytype = KeyAlgoED25519
1548+
pk1.Rest = Marshal(key)
1549+
case *ecdsa.PrivateKey:
1550+
var curve, keyType string
1551+
switch name := k.Curve.Params().Name; name {
1552+
case "P-256":
1553+
curve = "nistp256"
1554+
keyType = KeyAlgoECDSA256
1555+
case "P-384":
1556+
curve = "nistp384"
1557+
keyType = KeyAlgoECDSA384
1558+
case "P-521":
1559+
curve = "nistp521"
1560+
keyType = KeyAlgoECDSA521
1561+
default:
1562+
return nil, errors.New("ssh: unhandled elliptic curve " + name)
1563+
}
1564+
1565+
pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y)
1566+
1567+
// Marshal public key.
1568+
pubKey := struct {
1569+
KeyType string
1570+
Curve string
1571+
Pub []byte
1572+
}{
1573+
keyType, curve, pub,
1574+
}
1575+
w.PubKey = Marshal(pubKey)
1576+
1577+
// Marshal private key.
1578+
key := openSSHECDSAPrivateKey{
1579+
Curve: curve,
1580+
Pub: pub,
1581+
D: k.D,
1582+
Comment: comment,
1583+
}
1584+
pk1.Keytype = keyType
1585+
pk1.Rest = Marshal(key)
1586+
default:
1587+
return nil, fmt.Errorf("ssh: unsupported key type %T", k)
1588+
}
1589+
1590+
var err error
1591+
// Add padding and encrypt the key if necessary.
1592+
w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = encrypt(Marshal(pk1))
1593+
if err != nil {
1594+
return nil, err
1595+
}
1596+
1597+
b := Marshal(w)
1598+
block := &pem.Block{
1599+
Type: "OPENSSH PRIVATE KEY",
1600+
Bytes: append([]byte(privateKeyAuthMagic), b...),
1601+
}
1602+
return block, nil
1603+
}
1604+
14181605
func checkOpenSSHKeyPadding(pad []byte) error {
14191606
for i, b := range pad {
14201607
if int(b) != i+1 {
@@ -1424,6 +1611,13 @@ func checkOpenSSHKeyPadding(pad []byte) error {
14241611
return nil
14251612
}
14261613

1614+
func generateOpenSSHPadding(block []byte, blockSize int) []byte {
1615+
for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
1616+
block = append(block, byte(i+1))
1617+
}
1618+
return block
1619+
}
1620+
14271621
// FingerprintLegacyMD5 returns the user presentation of the key's
14281622
// fingerprint as described by RFC 4716 section 4.
14291623
func FingerprintLegacyMD5(pubKey PublicKey) string {

0 commit comments

Comments
 (0)