Mariano Cano has uploaded this change for review.
ssh: support for marshaling keys using the OpenSSH format
This adds methods to marshal private keys, encrypted and unecntypted
to the OpenSSH format.
Fixes golang/go#37132
Change-Id: I1a95301f789ce04858e6b147748c6e8b7700384b
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 289 insertions(+), 1 deletion(-)
diff --git a/ssh/keys.go b/ssh/keys.go
index d63cbf6..e3f3c29 100644
--- a/ssh/keys.go
+++ b/ssh/keys.go
@@ -13,11 +13,13 @@
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
+ "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
+ "encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
@@ -289,6 +291,18 @@
return b.Bytes()
}
+// MarshalPrivateKey returns a PEM block with the private key serialized in the
+// OpenSSH format.
+func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) {
+ return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaler)
+}
+
+// MarshalPrivateKeyWithPassphrase returns an encrypted PEM block with the
+// private key serialized in the OpenSSH format.
+func MarshalPrivateKeyWithPassphrase(key crypto.PrivateKey, comment string, passphrase []byte) (*pem.Block, error) {
+ return marshalOpenSSHPrivateKey(key, comment, passphraseProtectedOpenSSHMarshaler(passphrase))
+}
+
// PublicKey is an abstraction of different types of public keys.
type PublicKey interface {
// Type returns the key's type, e.g. "ssh-rsa".
@@ -1247,14 +1261,66 @@
}
}
+func randomSalt(size int) ([]byte, error) {
+ salt := make([]byte, size)
+ _, err := io.ReadFull(rand.Reader, salt)
+ if err != nil {
+ return nil, err
+ }
+ return salt, nil
+}
+
+func unencryptedOpenSSHMarshaler(PrivKeyBlock []byte) ([]byte, string, string, string, error) {
+ key := generateOpenSSHPadding(PrivKeyBlock, 8)
+ return key, "none", "none", "", nil
+}
+
+func passphraseProtectedOpenSSHMarshaler(passphrase []byte) openSSHEncryptFunc {
+ return func(PrivKeyBlock []byte) ([]byte, string, string, string, error) {
+ salt, err := randomSalt(16)
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ opts := struct {
+ Salt []byte
+ Rounds uint32
+ }{salt, 16}
+
+ // Derive key to encrypt the private key block.
+ k, err := bcrypt_pbkdf.Key(passphrase, salt, int(opts.Rounds), 32+aes.BlockSize)
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ // Add padding matching the block size of AES.
+ keyBlock := generateOpenSSHPadding(PrivKeyBlock, aes.BlockSize)
+
+ // Encrypt the private key using the derived secret.
+ dst := make([]byte, len(keyBlock))
+ iv := k[32 : 32+aes.BlockSize]
+ block, err := aes.NewCipher(k[:32])
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ stream := cipher.NewCTR(block, iv)
+ stream.XORKeyStream(dst, keyBlock)
+
+ return dst, "aes256-ctr", "bcrypt", string(Marshal(opts)), nil
+ }
+}
+
+const magic = "openssh-key-v1\x00"
+
type openSSHDecryptFunc func(CipherName, KdfName, KdfOpts string, PrivKeyBlock []byte) ([]byte, error)
+type openSSHEncryptFunc func(PrivKeyBlock []byte) (ProtectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error)
// parseOpenSSHPrivateKey parses an OpenSSH private key, using the decrypt
// function to unwrap the encrypted portion. unencryptedOpenSSHKey can be used
// as the decrypt function to parse an unencrypted private key. See
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.PrivateKey, error) {
- const magic = "openssh-key-v1\x00"
if len(key) < len(magic) || string(key[:len(magic)]) != magic {
return nil, errors.New("ssh: invalid openssh private key format")
}
@@ -1422,6 +1488,153 @@
}
}
+func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, encrypt openSSHEncryptFunc) (*pem.Block, error) {
+ var w struct {
+ CipherName string
+ KdfName string
+ KdfOpts string
+ NumKeys uint32
+ PubKey []byte
+ PrivKeyBlock []byte
+ }
+ var pk1 struct {
+ Check1 uint32
+ Check2 uint32
+ Keytype string
+ Rest []byte `ssh:"rest"`
+ }
+
+ // Random check bytes.
+ var check uint32
+ if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
+ return nil, err
+ }
+
+ pk1.Check1 = check
+ pk1.Check2 = check
+ w.NumKeys = 1
+
+ // Use a []byte directly on ed25519 keys.
+ if k, ok := key.(*ed25519.PrivateKey); ok {
+ key = *k
+ }
+
+ switch k := key.(type) {
+ case *rsa.PrivateKey:
+ E := new(big.Int).SetInt64(int64(k.PublicKey.E))
+ // Marshal public key:
+ // E and N are in reversed order in the public and private key.
+ pubKey := struct {
+ KeyType string
+ E *big.Int
+ N *big.Int
+ }{
+ KeyAlgoRSA,
+ E, k.PublicKey.N,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := struct {
+ N *big.Int
+ E *big.Int
+ D *big.Int
+ Iqmp *big.Int
+ P *big.Int
+ Q *big.Int
+ Comment string
+ }{
+ k.PublicKey.N, E,
+ k.D, k.Precomputed.Qinv, k.Primes[0], k.Primes[1],
+ comment,
+ }
+ pk1.Keytype = KeyAlgoRSA
+ pk1.Rest = Marshal(key)
+ case ed25519.PrivateKey:
+ pub := make([]byte, ed25519.PublicKeySize)
+ priv := make([]byte, ed25519.PrivateKeySize)
+ copy(pub, k[ed25519.PublicKeySize:])
+ copy(priv, k)
+
+ // Marshal public key.
+ pubKey := struct {
+ KeyType string
+ Pub []byte
+ }{
+ KeyAlgoED25519, pub,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := struct {
+ Pub []byte
+ Priv []byte
+ Comment string
+ }{
+ pub, priv,
+ comment,
+ }
+ pk1.Keytype = KeyAlgoED25519
+ pk1.Rest = Marshal(key)
+ case *ecdsa.PrivateKey:
+ var curve, keyType string
+ switch name := k.Curve.Params().Name; name {
+ case "P-256":
+ curve = "nistp256"
+ keyType = KeyAlgoECDSA256
+ case "P-384":
+ curve = "nistp384"
+ keyType = KeyAlgoECDSA384
+ case "P-521":
+ curve = "nistp521"
+ keyType = KeyAlgoECDSA521
+ default:
+ return nil, errors.New("ssh: unhandled elliptic curve " + name)
+ }
+
+ pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y)
+
+ // Marshal public key.
+ pubKey := struct {
+ KeyType string
+ Curve string
+ Pub []byte
+ }{
+ keyType, curve, pub,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := struct {
+ Curve string
+ Pub []byte
+ D *big.Int
+ Comment string
+ }{
+ curve, pub, k.D,
+ comment,
+ }
+ pk1.Keytype = keyType
+ pk1.Rest = Marshal(key)
+ default:
+ return nil, fmt.Errorf("ssh: unsupported key type %T", k)
+ }
+
+ var err error
+ // Add padding and encrypt the key if necessary.
+ w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = encrypt(Marshal(pk1))
+ if err != nil {
+ return nil, err
+ }
+
+ b := Marshal(w)
+ block := &pem.Block{
+ Type: "OPENSSH PRIVATE KEY",
+ Bytes: append([]byte(magic), b...),
+ }
+ return block, nil
+}
+
func checkOpenSSHKeyPadding(pad []byte) error {
for i, b := range pad {
if int(b) != i+1 {
@@ -1431,6 +1644,13 @@
return nil
}
+func generateOpenSSHPadding(block []byte, blockSize int) []byte {
+ for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
+ block = append(block, byte(i+1))
+ }
+ return block
+}
+
// FingerprintLegacyMD5 returns the user presentation of the key's
// fingerprint as described by RFC 4716 section 4.
func FingerprintLegacyMD5(pubKey PublicKey) string {
diff --git a/ssh/keys_test.go b/ssh/keys_test.go
index d64ef73..9d6f12f 100644
--- a/ssh/keys_test.go
+++ b/ssh/keys_test.go
@@ -281,6 +281,74 @@
}
}
+func TestMarshalPrivateKey(t *testing.T) {
+ tests := []struct {
+ name string
+ }{
+ {"rsa-openssh-format"},
+ {"ed25519"},
+ {"p256-openssh-format"},
+ {"p384-openssh-format"},
+ {"p521-openssh-format"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ expected, ok := testPrivateKeys[tt.name]
+ if !ok {
+ t.Fatalf("cannot find key %s", tt.name)
+ }
+
+ block, err := MarshalPrivateKey(expected, "te...@golang.org")
+ if err != nil {
+ t.Fatalf("cannot marshal %s: %v", tt.name, err)
+ }
+
+ key, err := ParseRawPrivateKey(pem.EncodeToMemory(block))
+ if err != nil {
+ t.Fatalf("cannot parse %s: %v", tt.name, err)
+ }
+
+ if !reflect.DeepEqual(expected, key) {
+ t.Errorf("unexpected marshaled key %s", tt.name)
+ }
+ })
+ }
+}
+
+func TestMarshalPrivateKeyWithPassphrase(t *testing.T) {
+ tests := []struct {
+ name string
+ }{
+ {"rsa-openssh-format"},
+ {"ed25519"},
+ {"p256-openssh-format"},
+ {"p384-openssh-format"},
+ {"p521-openssh-format"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ expected, ok := testPrivateKeys[tt.name]
+ if !ok {
+ t.Fatalf("cannot find key %s", tt.name)
+ }
+
+ block, err := MarshalPrivateKeyWithPassphrase(expected, "te...@golang.org", []byte("test-passphrase"))
+ if err != nil {
+ t.Fatalf("cannot marshal %s: %v", tt.name, err)
+ }
+
+ key, err := ParseRawPrivateKeyWithPassphrase(pem.EncodeToMemory(block), []byte("test-passphrase"))
+ if err != nil {
+ t.Fatalf("cannot parse %s: %v", tt.name, err)
+ }
+
+ if !reflect.DeepEqual(expected, key) {
+ t.Errorf("unexpected marshaled key %s", tt.name)
+ }
+ })
+ }
+}
+
type testAuthResult struct {
pubKey PublicKey
options []string
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Congratulations on opening your first change. Thank you for your contribution!
Next steps:
Within the next week or so, a maintainer will review your change and provide
feedback. See https://golang.org/doc/contribute.html#review for more info and
tips to get your patch through code review.
Most changes in the Go project go through a few rounds of revision. This can be
surprising to people new to the project. The careful, iterative review process
is our way of helping mentor contributors and ensuring that their contributions
have a lasting impact.
During May-July and Nov-Jan the Go project is in a code freeze, during which
little code gets reviewed or merged. If a reviewer responds with a comment like
R=go1.11, it means that this CL will be reviewed as part of the next development
cycle. See https://golang.org/s/release for more details.
Hi Filippo and Han-Wen,
I've pushed some code to marshal private keys to the OpenSSH format.
I think having full interoperability in terms of keys with OpenSSH would be a good enhancement for the x/crypto/ssh package.
Salut
1 comment:
Patchset:
Hi Katie, is there any update on this PR?
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel.
2 comments:
File ssh/keys.go:
Patch Set #2, Line 300: // MarshalPrivateKeyWithPassphrase returns an encrypted PEM block with the
nit: the PEM block itself isn't encrypted (it is still plaintext). […]
Do you mean changing the docs to something like:
MarshalPrivateKeyWithPassphrase returns a PEM block holding the encrypted private key serialized in the OpenSSH format.
Patch Set #2, Line 1288: if err != nil {
nit: I think convention elsewhere in the code base would be to handle this directly in passphrasePro […]
Ack
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Mariano Cano.
Mariano Cano uploaded patch set #3 to this change.
ssh: support for marshaling keys using the OpenSSH format
This adds methods to marshal private keys, encrypted and unecntypted
to the OpenSSH format.
Fixes golang/go#37132
Change-Id: I1a95301f789ce04858e6b147748c6e8b7700384b
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 294 insertions(+), 1 deletion(-)
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
File ssh/keys.go:
Patch Set #2, Line 300: // MarshalPrivateKeyWithPassphrase returns an encrypted PEM block with the
Yeah, I think that makes sense. […]
Done
Patch Set #2, Line 1288: if err != nil {
Ack
Done
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Mariano Cano.
1 comment:
File ssh/keys.go:
Patch Set #2, Line 300: // MarshalPrivateKeyWithPassphrase returns an encrypted PEM block with the
Do you mean changing the docs to something like: […]
Yeah, I think that makes sense.
I've got a branch here for testing this with Vault's SSH engine: https://github.com/cipherboy/vault/commit/switch-ssh-crypto
Works well and many thanks for this patchset!
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Mariano Cano.
Patch set 3:Code-Review +1
Attention is currently required from: Mariano Cano.
1 comment:
File ssh/keys.go:
Patch Set #2, Line 1288: if err != nil {
Ack
Ack
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Mariano Cano.
Patch set 2:Code-Review +1
3 comments:
Patchset:
Not part of the Golang team; just a drive by review of two nits with the proposed patch.
File ssh/keys.go:
Patch Set #2, Line 300: // MarshalPrivateKeyWithPassphrase returns an encrypted PEM block with the
nit: the PEM block itself isn't encrypted (it is still plaintext). The serialized format contains an encrypted private key using the specified KDF/...
Patch Set #2, Line 1288: if err != nil {
nit: I think convention elsewhere in the code base would be to handle this directly in passphraseProtectedOpenSSHMarshaler, like e.g. mime/multipart/writer.go or crypto/tls/key_schedule.go. Especially since it appears to only be used once?
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel.
1 comment:
Patchset:
Not part of the Golang team; just a drive by review of two nits with the proposed patch.
Ack
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel, Mariano Cano.
Patch set 3:Code-Review +1
1 comment:
Patchset:
Hello. Just wanted to say that at HashiCorp we are working on an update to a Terraform Provider (`terraform-provider-tls`) and we have "cherry-picked" this change and temporarily included it in our codebase (https://github.com/hashicorp/terraform-provider-tls/pull/151).
The plan is to rip it out once a version of `x/crypto/ssh` with this change applied is released.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel, Mariano Cano.
Patch set 3:Run-TryBot +1Code-Review +1Trust +1
2 comments:
Commit Message:
Patch Set #3, Line 9: This adds methods to marshal private keys, encrypted and unecntypted
*unencrypted
Patchset:
LGTM pending typo fix from a non-cryptogopher perspective.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel, Mariano Cano.
Mariano Cano uploaded patch set #4 to this change.
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
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 294 insertions(+), 1 deletion(-)
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Alex Scheel, Matt Layher.
1 comment:
Commit Message:
Patch Set #3, Line 9: This adds methods to marshal private keys, encrypted and unecntypted
*unencrypted
Done
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Matt Layher.
Patch set 4:Code-Review +1
Attention is currently required from: Matt Layher, Mariano Cano.
3 comments:
Patchset:
Hey, great patch!
Did a couple of comments, but I'm not a Go team member, just poking around ed25519 keys and found this patch, so take them with a grain of salt.
Thanks!
File ssh/keys.go:
Patch Set #4, Line 1294: func unencryptedOpenSSHMarshaler(PrivKeyBlock []byte) ([]byte, string, string, string, error) {
I think the convention would be to `PrivKeyBlock` start with a lowercase letter.
The same issue happens in other places with this var name as well.
File ssh/keys_test.go:
Patch Set #4, Line 335: block, err := MarshalPrivateKeyWithPassphrase(expected, "te...@golang.org", []byte("test-passphrase"))
should we also test the output is the same as the one generated by OpenSSH's `ssh-keygen`?
I tested this changeset on a ed25519 key:
The resulting private key is a bit different.
More specifically, the line breaks are on different places, not sure how bad that is.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Matt Layher, Mariano Cano.
Mariano Cano uploaded patch set #5 to this change.
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
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 294 insertions(+), 1 deletion(-)
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Matt Layher.
3 comments:
Patchset:
Hey, great patch! […]
Ack
File ssh/keys.go:
Patch Set #4, Line 1294: func unencryptedOpenSSHMarshaler(PrivKeyBlock []byte) ([]byte, string, string, string, error) {
I think the convention would be to `PrivKeyBlock` start with a lowercase letter. […]
Ack
File ssh/keys_test.go:
Patch Set #4, Line 335: block, err := MarshalPrivateKeyWithPassphrase(expected, "te...@golang.org", []byte("test-passphrase"))
should we also test the output is the same as the one generated by OpenSSH's `ssh-keygen`? […]
The methods added in this pull request return a *pem.Block, that is basically a type for the header and the bytes, how are they encoded later depends on the implementation of encoding/pem.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano, Matt Layher, Roland Shoemaker.
1 comment:
Patchset:
This CL has picked up a merge conflict.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano, Matt Layher.
9 comments:
File ssh/keys.go:
Patch Set #5, Line 1302: io.ReadFull
nit: this can just be `rand.Read(salt)`.
iv := k[32 : 32+aes.BlockSize]
block, err := aes.NewCipher(k[:32])
This and the below slice operation are slightly more readable by doing `key, iv := k[:32], k[32:]`, as is done in `passphraseProtectedOpenSSHKey`.
Patch Set #5, Line 1335: const magic = "openssh-key-v1\x00"
Change this to `privateKeyAuthMagic` and add a comment indicating this is the header prefix indicating a private key block.
var w struct {
CipherName string
KdfName string
KdfOpts string
NumKeys uint32
PubKey []byte
PrivKeyBlock []byte
}
var pk1 struct {
Check1 uint32
Check2 uint32
Keytype string
Rest []byte `ssh:"rest"`
}
These structs are also used in parseOpenSSHPrivateKey, might want to just lift them out so that the same types are used in both places.
// Use a []byte directly on ed25519 keys.
if k, ok := key.(*ed25519.PrivateKey); ok {
key = *k
}
This can be skipped, and the key can be directly deref'd in the switch below.
struct {
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int
P *big.Int
Q *big.Int
Comment string
}
Similarly, through this function we are duplicating types from parseOpenSSHPrivateKey.
Patch Set #5, Line 1576: ed25519.PublicKeySize:
While this is technically correct, it is somewhat confusing to the casual reader since it appears you are reading _past_ the public key (this works because `PrivateKeySize = 2*PublicKeySize`). Might be (weirdly) more readable as just `32`.
copy(pub, k[ed25519.PublicKeySize:])
copy(priv, k)
i.e. this can be
copy(pub, (*k)[ed25519.PublicKeySize:])
copy(priv, *k)
func generateOpenSSHPadding(block []byte, blockSize int) []byte {
for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
block = append(block, byte(i+1))
}
return block
}
Padding can be generated in one shot as
func generateOpenSSHPadding(block []byte, blockSize int) []byte {
paddingNeeded := blockSize - (len(block) % blockSize)
if paddingNeeded > 0 {
block = append(block, make([]byte, paddingNeeded)...)
}
return block
}
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano, Matt Layher.
1 comment:
File ssh/keys.go:
func generateOpenSSHPadding(block []byte, blockSize int) []byte {
for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
block = append(block, byte(i+1))
}
return block
}
Padding can be generated in one shot as […]
Oh never mind, I forgot the weird OpenSSH padding scheme 🤦.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano.
Mariano Cano uploaded patch set #6 to this change.
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
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 322 insertions(+), 46 deletions(-)
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Roland Shoemaker.
8 comments:
File ssh/keys.go:
Patch Set #5, Line 1302: io.ReadFull
nit: this can just be `rand.Read(salt)`.
Ack
iv := k[32 : 32+aes.BlockSize]
block, err := aes.NewCipher(k[:32])
This and the below slice operation are slightly more readable by doing `key, iv := k[:32], k[32:]`, […]
Ack
Patch Set #5, Line 1335: const magic = "openssh-key-v1\x00"
Change this to `privateKeyAuthMagic` and add a comment indicating this is the header prefix indicati […]
Ack
var w struct {
CipherName string
KdfName string
KdfOpts string
NumKeys uint32
PubKey []byte
PrivKeyBlock []byte
}
var pk1 struct {
Check1 uint32
Check2 uint32
Keytype string
Rest []byte `ssh:"rest"`
}
These structs are also used in parseOpenSSHPrivateKey, might want to just lift them out so that the […]
Ack
// Use a []byte directly on ed25519 keys.
if k, ok := key.(*ed25519.PrivateKey); ok {
key = *k
}
This can be skipped, and the key can be directly deref'd in the switch below.
I don't think it is a good idea to repeat the entire `case` statement, and with type switches, you cannot use `fallthrough`.
This `if` condition is there because some other ssh code in the `ssh` package supports that case, but it's not the general idiom.
struct {
N *big.Int
E *big.Int
D *big.Int
Iqmp *big.Int
P *big.Int
Q *big.Int
Comment string
}
Similarly, through this function we are duplicating types from parseOpenSSHPrivateKey.
Ack
Patch Set #5, Line 1576: ed25519.PublicKeySize:
While this is technically correct, it is somewhat confusing to the casual reader since it appears yo […]
Ack
copy(pub, k[ed25519.PublicKeySize:])
copy(priv, k)
i.e. this can be […]
Per before, I don't think is a good idea to add a `case *ed25519.PrivateKey1`
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Mariano Cano, Roland Shoemaker.
2 comments:
Patchset:
just wondering if we can clarify the key format, or at least the 2 check ints.
File ssh/keys.go:
Patch Set #6, Line 1316: Check2 uint32
are those check fields the "64-bit dummy checksum? # a random 32-bit int, repeated" field from the openssh format[1]?
[1]: https://coolaj86.com/articles/the-openssh-private-key-format/
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Roland Shoemaker.
3 comments:
File ssh/keys.go:
// Use a []byte directly on ed25519 keys.
if k, ok := key.(*ed25519.PrivateKey); ok {
key = *k
}
I don't think it is a good idea to repeat the entire `case` statement, and with type switches, you c […]
Done
copy(pub, k[ed25519.PublicKeySize:])
copy(priv, k)
Per before, I don't think is a good idea to add a `case *ed25519. […]
Done
File ssh/keys.go:
Patch Set #6, Line 1316: Check2 uint32
are those check fields the "64-bit dummy checksum? # a random 32-bit int, repeated" field from the […]
These checksums are described at https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
When you encrypt a key, a random value is assigned to both checks, when you decrypt it, if the value is different you know for sure that the password was not the correct one, and you don't need to attempt to unmarshal the rest.
With an incorrect password, you can also get those checks to represent the right value, but then the probability of properly unmarshal a valid key is negligible.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano, Roland Shoemaker.
1 comment:
Patchset:
Ping. Isn't this waiting on another round of review?
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano, Roland Shoemaker.
1 comment:
Patchset:
Hi Roland et al, could we please get this reviewed? The author has been waiting 3.5 years now and more than 6 months since the most recent replies.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano.
Patch set 6:Run-TryBot +1Code-Review +2Commit-Queue +1
1 comment:
Patchset:
Sorry for the (extremely) long wait on this, sometimes CLs get buried in my dashboard and it can be quite hard for them to resurface.
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano.
Patch set 7:Run-TryBot +1Code-Review +2Commit-Queue +1
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano.
Patch set 7:Auto-Submit +1
Attention is currently required from: Carlos Alexandro Becker, Mariano Cano.
Patch set 7:Code-Review +1
Gopher Robot submitted this change.
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 <rol...@golang.org>
TryBot-Result: Gopher Robot <go...@golang.org>
LUCI-TryBot-Result: Go LUCI <golang...@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cher...@google.com>
Reviewed-by: Roland Shoemaker <rol...@golang.org>
Auto-Submit: Roland Shoemaker <rol...@golang.org>
---
M ssh/keys.go
M ssh/keys_test.go
2 files changed, 308 insertions(+), 46 deletions(-)
diff --git a/ssh/keys.go b/ssh/keys.go
index dac8ee7..1bf28d8 100644
--- a/ssh/keys.go
+++ b/ssh/keys.go
@@ -13,11 +13,13 @@
"crypto/ecdsa"
"crypto/elliptic"
"crypto/md5"
+ "crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
+ "encoding/binary"
"encoding/hex"
"encoding/pem"
"errors"
@@ -295,6 +297,18 @@
return b.Bytes()
}
+// MarshalPrivateKey returns a PEM block with the private key serialized in the
+// OpenSSH format.
+func MarshalPrivateKey(key crypto.PrivateKey, comment string) (*pem.Block, error) {
+ return marshalOpenSSHPrivateKey(key, comment, unencryptedOpenSSHMarshaler)
+}
+
+// MarshalPrivateKeyWithPassphrase returns a PEM block holding the encrypted
+// private key serialized in the OpenSSH format.
+func MarshalPrivateKeyWithPassphrase(key crypto.PrivateKey, comment string, passphrase []byte) (*pem.Block, error) {
+ return marshalOpenSSHPrivateKey(key, comment, passphraseProtectedOpenSSHMarshaler(passphrase))
+}
+
// PublicKey represents a public key using an unspecified algorithm.
//
// Some PublicKeys provided by this package also implement CryptoPublicKey.
@@ -1241,28 +1255,106 @@
}
}
+func unencryptedOpenSSHMarshaler(privKeyBlock []byte) ([]byte, string, string, string, error) {
+ key := generateOpenSSHPadding(privKeyBlock, 8)
+ return key, "none", "none", "", nil
+}
+
+func passphraseProtectedOpenSSHMarshaler(passphrase []byte) openSSHEncryptFunc {
+ return func(privKeyBlock []byte) ([]byte, string, string, string, error) {
+ salt := make([]byte, 16)
+ if _, err := rand.Read(salt); err != nil {
+ return nil, "", "", "", err
+ }
+
+ opts := struct {
+ Salt []byte
+ Rounds uint32
+ }{salt, 16}
+
+ // Derive key to encrypt the private key block.
+ k, err := bcrypt_pbkdf.Key(passphrase, salt, int(opts.Rounds), 32+aes.BlockSize)
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ // Add padding matching the block size of AES.
+ keyBlock := generateOpenSSHPadding(privKeyBlock, aes.BlockSize)
+
+ // Encrypt the private key using the derived secret.
+
+ dst := make([]byte, len(keyBlock))
+ key, iv := k[:32], k[32:]
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, "", "", "", err
+ }
+
+ stream := cipher.NewCTR(block, iv)
+ stream.XORKeyStream(dst, keyBlock)
+
+ return dst, "aes256-ctr", "bcrypt", string(Marshal(opts)), nil
+ }
+}
+
+const privateKeyAuthMagic = "openssh-key-v1\x00"
+
type openSSHDecryptFunc func(CipherName, KdfName, KdfOpts string, PrivKeyBlock []byte) ([]byte, error)
+type openSSHEncryptFunc func(PrivKeyBlock []byte) (ProtectedKeyBlock []byte, cipherName, kdfName, kdfOptions string, err error)
+
+type openSSHEncryptedPrivateKey struct {
+ CipherName string
+ KdfName string
+ KdfOpts string
+ NumKeys uint32
+ PubKey []byte
+ PrivKeyBlock []byte
+}
+
+type openSSHPrivateKey struct {
+ Check1 uint32
+ Check2 uint32
+ Keytype string
+ Rest []byte `ssh:"rest"`
+}
+
+type openSSHRSAPrivateKey struct {
+ N *big.Int
+ E *big.Int
+ D *big.Int
+ Iqmp *big.Int
+ P *big.Int
+ Q *big.Int
+ Comment string
+ Pad []byte `ssh:"rest"`
+}
+
+type openSSHEd25519PrivateKey struct {
+ Pub []byte
+ Priv []byte
+ Comment string
+ Pad []byte `ssh:"rest"`
+}
+
+type openSSHECDSAPrivateKey struct {
+ Curve string
+ Pub []byte
+ D *big.Int
+ Comment string
+ Pad []byte `ssh:"rest"`
+}
// parseOpenSSHPrivateKey parses an OpenSSH private key, using the decrypt
// function to unwrap the encrypted portion. unencryptedOpenSSHKey can be used
// as the decrypt function to parse an unencrypted private key. See
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
func parseOpenSSHPrivateKey(key []byte, decrypt openSSHDecryptFunc) (crypto.PrivateKey, error) {
- const magic = "openssh-key-v1\x00"
- if len(key) < len(magic) || string(key[:len(magic)]) != magic {
+ if len(key) < len(privateKeyAuthMagic) || string(key[:len(privateKeyAuthMagic)]) != privateKeyAuthMagic {
return nil, errors.New("ssh: invalid openssh private key format")
}
- remaining := key[len(magic):]
+ remaining := key[len(privateKeyAuthMagic):]
- var w struct {
- CipherName string
- KdfName string
- KdfOpts string
- NumKeys uint32
- PubKey []byte
- PrivKeyBlock []byte
- }
-
+ var w openSSHEncryptedPrivateKey
if err := Unmarshal(remaining, &w); err != nil {
return nil, err
}
@@ -1284,13 +1376,7 @@
return nil, err
}
- pk1 := struct {
- Check1 uint32
- Check2 uint32
- Keytype string
- Rest []byte `ssh:"rest"`
- }{}
-
+ var pk1 openSSHPrivateKey
if err := Unmarshal(privKeyBlock, &pk1); err != nil || pk1.Check1 != pk1.Check2 {
if w.CipherName != "none" {
return nil, x509.IncorrectPasswordError
@@ -1300,18 +1386,7 @@
switch pk1.Keytype {
case KeyAlgoRSA:
- // https://github.com/openssh/openssh-portable/blob/master/sshkey.c#L2760-L2773
- key := struct {
- N *big.Int
- E *big.Int
- D *big.Int
- Iqmp *big.Int
- P *big.Int
- Q *big.Int
- Comment string
- Pad []byte `ssh:"rest"`
- }{}
-
+ var key openSSHRSAPrivateKey
if err := Unmarshal(pk1.Rest, &key); err != nil {
return nil, err
}
@@ -1337,13 +1412,7 @@
return pk, nil
case KeyAlgoED25519:
- key := struct {
- Pub []byte
- Priv []byte
- Comment string
- Pad []byte `ssh:"rest"`
- }{}
-
+ var key openSSHEd25519PrivateKey
if err := Unmarshal(pk1.Rest, &key); err != nil {
return nil, err
}
@@ -1360,14 +1429,7 @@
copy(pk, key.Priv)
return &pk, nil
case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521:
- key := struct {
- Curve string
- Pub []byte
- D *big.Int
- Comment string
- Pad []byte `ssh:"rest"`
- }{}
-
+ var key openSSHECDSAPrivateKey
if err := Unmarshal(pk1.Rest, &key); err != nil {
return nil, err
}
@@ -1415,6 +1477,131 @@
}
}
+func marshalOpenSSHPrivateKey(key crypto.PrivateKey, comment string, encrypt openSSHEncryptFunc) (*pem.Block, error) {
+ var w openSSHEncryptedPrivateKey
+ var pk1 openSSHPrivateKey
+
+ // Random check bytes.
+ var check uint32
+ if err := binary.Read(rand.Reader, binary.BigEndian, &check); err != nil {
+ return nil, err
+ }
+
+ pk1.Check1 = check
+ pk1.Check2 = check
+ w.NumKeys = 1
+
+ // Use a []byte directly on ed25519 keys.
+ if k, ok := key.(*ed25519.PrivateKey); ok {
+ key = *k
+ }
+
+ switch k := key.(type) {
+ case *rsa.PrivateKey:
+ E := new(big.Int).SetInt64(int64(k.PublicKey.E))
+ // Marshal public key:
+ // E and N are in reversed order in the public and private key.
+ pubKey := struct {
+ KeyType string
+ E *big.Int
+ N *big.Int
+ }{
+ KeyAlgoRSA,
+ E, k.PublicKey.N,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := openSSHRSAPrivateKey{
+ N: k.PublicKey.N,
+ E: E,
+ D: k.D,
+ Iqmp: k.Precomputed.Qinv,
+ P: k.Primes[0],
+ Q: k.Primes[1],
+ Comment: comment,
+ }
+ pk1.Keytype = KeyAlgoRSA
+ pk1.Rest = Marshal(key)
+ case ed25519.PrivateKey:
+ pub := make([]byte, ed25519.PublicKeySize)
+ priv := make([]byte, ed25519.PrivateKeySize)
+ copy(pub, k[32:])
+ copy(priv, k)
+
+ // Marshal public key.
+ pubKey := struct {
+ KeyType string
+ Pub []byte
+ }{
+ KeyAlgoED25519, pub,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := openSSHEd25519PrivateKey{
+ Pub: pub,
+ Priv: priv,
+ Comment: comment,
+ }
+ pk1.Keytype = KeyAlgoED25519
+ pk1.Rest = Marshal(key)
+ case *ecdsa.PrivateKey:
+ var curve, keyType string
+ switch name := k.Curve.Params().Name; name {
+ case "P-256":
+ curve = "nistp256"
+ keyType = KeyAlgoECDSA256
+ case "P-384":
+ curve = "nistp384"
+ keyType = KeyAlgoECDSA384
+ case "P-521":
+ curve = "nistp521"
+ keyType = KeyAlgoECDSA521
+ default:
+ return nil, errors.New("ssh: unhandled elliptic curve " + name)
+ }
+
+ pub := elliptic.Marshal(k.Curve, k.PublicKey.X, k.PublicKey.Y)
+
+ // Marshal public key.
+ pubKey := struct {
+ KeyType string
+ Curve string
+ Pub []byte
+ }{
+ keyType, curve, pub,
+ }
+ w.PubKey = Marshal(pubKey)
+
+ // Marshal private key.
+ key := openSSHECDSAPrivateKey{
+ Curve: curve,
+ Pub: pub,
+ D: k.D,
+ Comment: comment,
+ }
+ pk1.Keytype = keyType
+ pk1.Rest = Marshal(key)
+ default:
+ return nil, fmt.Errorf("ssh: unsupported key type %T", k)
+ }
+
+ var err error
+ // Add padding and encrypt the key if necessary.
+ w.PrivKeyBlock, w.CipherName, w.KdfName, w.KdfOpts, err = encrypt(Marshal(pk1))
+ if err != nil {
+ return nil, err
+ }
+
+ b := Marshal(w)
+ block := &pem.Block{
+ Type: "OPENSSH PRIVATE KEY",
+ Bytes: append([]byte(privateKeyAuthMagic), b...),
+ }
+ return block, nil
+}
+
func checkOpenSSHKeyPadding(pad []byte) error {
for i, b := range pad {
if int(b) != i+1 {
@@ -1424,6 +1611,13 @@
return nil
}
+func generateOpenSSHPadding(block []byte, blockSize int) []byte {
+ for i, l := 0, len(block); (l+i)%blockSize != 0; i++ {
+ block = append(block, byte(i+1))
+ }
+ return block
+}
+
// FingerprintLegacyMD5 returns the user presentation of the key's
// fingerprint as described by RFC 4716 section 4.
func FingerprintLegacyMD5(pubKey PublicKey) string {
diff --git a/ssh/keys_test.go b/ssh/keys_test.go
index 334ef74..a8e216e 100644
--- a/ssh/keys_test.go
+++ b/ssh/keys_test.go
@@ -281,6 +281,74 @@
}
}
+func TestMarshalPrivateKey(t *testing.T) {
+ tests := []struct {
+ name string
+ }{
+ {"rsa-openssh-format"},
+ {"ed25519"},
+ {"p256-openssh-format"},
+ {"p384-openssh-format"},
+ {"p521-openssh-format"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ expected, ok := testPrivateKeys[tt.name]
+ if !ok {
+ t.Fatalf("cannot find key %s", tt.name)
+ }
+
+ block, err := MarshalPrivateKey(expected, "te...@golang.org")
+ if err != nil {
+ t.Fatalf("cannot marshal %s: %v", tt.name, err)
+ }
+
+ key, err := ParseRawPrivateKey(pem.EncodeToMemory(block))
+ if err != nil {
+ t.Fatalf("cannot parse %s: %v", tt.name, err)
+ }
+
+ if !reflect.DeepEqual(expected, key) {
+ t.Errorf("unexpected marshaled key %s", tt.name)
+ }
+ })
+ }
+}
+
+func TestMarshalPrivateKeyWithPassphrase(t *testing.T) {
+ tests := []struct {
+ name string
+ }{
+ {"rsa-openssh-format"},
+ {"ed25519"},
+ {"p256-openssh-format"},
+ {"p384-openssh-format"},
+ {"p521-openssh-format"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ expected, ok := testPrivateKeys[tt.name]
+ if !ok {
+ t.Fatalf("cannot find key %s", tt.name)
+ }
+
+ block, err := MarshalPrivateKeyWithPassphrase(expected, "te...@golang.org", []byte("test-passphrase"))
+ if err != nil {
+ t.Fatalf("cannot marshal %s: %v", tt.name, err)
+ }
+
+ key, err := ParseRawPrivateKeyWithPassphrase(pem.EncodeToMemory(block), []byte("test-passphrase"))
+ if err != nil {
+ t.Fatalf("cannot parse %s: %v", tt.name, err)
+ }
+
+ if !reflect.DeepEqual(expected, key) {
+ t.Errorf("unexpected marshaled key %s", tt.name)
+ }
+ })
+ }
+}
+
type testAuthResult struct {
pubKey PublicKey
options []string
To view, visit change 218620. To unsubscribe, or for help writing mail filters, visit settings.