From 1e61df8d9ea476e2e1504cd9a32b40280c7c6c7e Mon Sep 17 00:00:00 2001 From: Martin Garton Date: Sat, 30 Apr 2016 22:10:58 +0100 Subject: [PATCH] x/crypto/ssh: add support for ed25519 keys Added support for parsing the "new" openssh private key format. (ed25519 keys only in this format for now) Signing and verifying functions now work with ed25519 keys. ed25519 can now be accepted by the server to authenticate a client. ed25519 can now be accepted by a client as a server host key. Related documentation used: https://www.ietf.org/archive/id/draft-bjh21-ssh-ed25519-02.txt Change-Id: I84385f24d666fea08de21f980f78623f7bff8007 Reviewed-on: https://go-review.googlesource.com/22512 Reviewed-by: Han-Wen Nienhuys Run-TryBot: Han-Wen Nienhuys --- ssh/agent/client.go | 2 +- ssh/certs.go | 4 +- ssh/common.go | 2 + ssh/keys.go | 136 ++++++++++++++++++++++++++++++++++--- ssh/keys_test.go | 3 + ssh/server.go | 2 +- ssh/test/session_test.go | 25 +++++++ ssh/test/test_unix_test.go | 8 ++- ssh/testdata/keys.go | 8 +++ 9 files changed, 178 insertions(+), 12 deletions(-) diff --git a/ssh/agent/client.go b/ssh/agent/client.go index 6f794ef..3f798e7 100644 --- a/ssh/agent/client.go +++ b/ssh/agent/client.go @@ -188,7 +188,7 @@ func (k *Key) Marshal() []byte { func (k *Key) Verify(data []byte, sig *ssh.Signature) error { pubKey, err := ssh.ParsePublicKey(k.Blob) if err != nil { - return fmt.Errorf("agent: bad public key") + return fmt.Errorf("agent: bad public key: %v", err) } return pubKey.Verify(data, sig) } diff --git a/ssh/certs.go b/ssh/certs.go index 3857700..6331c94 100644 --- a/ssh/certs.go +++ b/ssh/certs.go @@ -22,6 +22,7 @@ const ( CertAlgoECDSA256v01 = "ecdsa-sha2-nistp256-cert-v01@openssh.com" CertAlgoECDSA384v01 = "ecdsa-sha2-nistp384-cert-v01@openssh.com" CertAlgoECDSA521v01 = "ecdsa-sha2-nistp521-cert-v01@openssh.com" + CertAlgoED25519v01 = "ssh-ed25519-cert-v01@openssh.com" ) // Certificate types distinguish between host and user @@ -401,6 +402,7 @@ var certAlgoNames = map[string]string{ KeyAlgoECDSA256: CertAlgoECDSA256v01, KeyAlgoECDSA384: CertAlgoECDSA384v01, KeyAlgoECDSA521: CertAlgoECDSA521v01, + KeyAlgoED25519: CertAlgoED25519v01, } // certToPrivAlgo returns the underlying algorithm for a certificate algorithm. @@ -459,7 +461,7 @@ func (c *Certificate) Marshal() []byte { func (c *Certificate) Type() string { algo, ok := certAlgoNames[c.Key.Type()] if !ok { - panic("unknown cert key type") + panic("unknown cert key type " + c.Key.Type()) } return algo } diff --git a/ssh/common.go b/ssh/common.go index 9fc739e..de029d6 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -48,6 +48,8 @@ var supportedHostKeyAlgos = []string{ KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoRSA, KeyAlgoDSA, + + KeyAlgoED25519, } // supportedMACs specifies a default set of MAC algorithms in preference order. diff --git a/ssh/keys.go b/ssh/keys.go index 25be3c5..d6167e7 100644 --- a/ssh/keys.go +++ b/ssh/keys.go @@ -20,6 +20,8 @@ import ( "io" "math/big" "strings" + + "golang.org/x/crypto/ed25519" ) // These constants represent the algorithm names for key types supported by this @@ -30,6 +32,7 @@ const ( KeyAlgoECDSA256 = "ecdsa-sha2-nistp256" KeyAlgoECDSA384 = "ecdsa-sha2-nistp384" KeyAlgoECDSA521 = "ecdsa-sha2-nistp521" + KeyAlgoED25519 = "ssh-ed25519" ) // parsePubKey parses a public key of the given algorithm. @@ -42,14 +45,16 @@ func parsePubKey(in []byte, algo string) (pubKey PublicKey, rest []byte, err err return parseDSA(in) case KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521: return parseECDSA(in) - case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01: + case KeyAlgoED25519: + return parseED25519(in) + case CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoED25519v01: cert, err := parseCert(in, certToPrivAlgo(algo)) if err != nil { return nil, nil, err } return cert, nil, nil } - return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", err) + return nil, nil, fmt.Errorf("ssh: unknown key algorithm: %v", algo) } // parseAuthorizedKey parses a public key in OpenSSH authorized_keys format @@ -459,6 +464,51 @@ func (key *ecdsaPublicKey) nistID() string { panic("ssh: unsupported ecdsa key size") } +type ed25519PublicKey ed25519.PublicKey + +func (key ed25519PublicKey) Type() string { + return KeyAlgoED25519 +} + +func parseED25519(in []byte) (out PublicKey, rest []byte, err error) { + var w struct { + KeyBytes []byte + Rest []byte `ssh:"rest"` + } + + if err := Unmarshal(in, &w); err != nil { + return nil, nil, err + } + + key := ed25519.PublicKey(w.KeyBytes) + + return (ed25519PublicKey)(key), w.Rest, nil +} + +func (key ed25519PublicKey) Marshal() []byte { + w := struct { + Name string + KeyBytes []byte + }{ + KeyAlgoED25519, + []byte(key), + } + return Marshal(&w) +} + +func (key ed25519PublicKey) Verify(b []byte, sig *Signature) error { + if sig.Format != key.Type() { + return fmt.Errorf("ssh: signature type %s for key type %s", sig.Format, key.Type()) + } + + edKey := (ed25519.PublicKey)(key) + if ok := ed25519.Verify(edKey, b, sig.Blob); !ok { + return errors.New("ssh: signature did not verify") + } + + return nil +} + func supportedEllipticCurve(curve elliptic.Curve) bool { return curve == elliptic.P256() || curve == elliptic.P384() || curve == elliptic.P521() } @@ -597,13 +647,19 @@ func (s *wrappedSigner) Sign(rand io.Reader, data []byte) (*Signature, error) { hashFunc = crypto.SHA1 case *ecdsaPublicKey: hashFunc = ecHash(key.Curve) + case ed25519PublicKey: default: return nil, fmt.Errorf("ssh: unsupported key type %T", key) } - h := hashFunc.New() - h.Write(data) - digest := h.Sum(nil) + var digest []byte + if hashFunc != 0 { + h := hashFunc.New() + h.Write(data) + digest = h.Sum(nil) + } else { + digest = data + } signature, err := s.signer.Sign(rand, digest, hashFunc) if err != nil { @@ -643,9 +699,9 @@ func (s *wrappedSigner) Sign(rand io.Reader, data []byte) (*Signature, error) { }, nil } -// NewPublicKey takes an *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey or -// any other crypto.Signer and returns a corresponding Signer instance. ECDSA -// keys must use P-256, P-384 or P-521. +// NewPublicKey takes an *rsa.PublicKey, *dsa.PublicKey, *ecdsa.PublicKey, +// ed25519.PublicKey, or any other crypto.Signer and returns a corresponding +// Signer instance. ECDSA keys must use P-256, P-384 or P-521. func NewPublicKey(key interface{}) (PublicKey, error) { switch key := key.(type) { case *rsa.PublicKey: @@ -657,6 +713,8 @@ func NewPublicKey(key interface{}) (PublicKey, error) { return (*ecdsaPublicKey)(key), nil case *dsa.PublicKey: return (*dsaPublicKey)(key), nil + case ed25519.PublicKey: + return (ed25519PublicKey)(key), nil default: return nil, fmt.Errorf("ssh: unsupported key type %T", key) } @@ -688,6 +746,8 @@ func ParseRawPrivateKey(pemBytes []byte) (interface{}, error) { return x509.ParseECPrivateKey(block.Bytes) case "DSA PRIVATE KEY": return ParseDSAPrivateKey(block.Bytes) + case "OPENSSH PRIVATE KEY": + return parseOpenSSHPrivateKey(block.Bytes) default: return nil, fmt.Errorf("ssh: unsupported key type %q", block.Type) } @@ -724,3 +784,63 @@ func ParseDSAPrivateKey(der []byte) (*dsa.PrivateKey, error) { X: k.Pub, }, nil } + +// Implemented based on the documentation at +// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key +func parseOpenSSHPrivateKey(key []byte) (*ed25519.PrivateKey, error) { + magic := append([]byte("openssh-key-v1"), 0) + if !bytes.Equal(magic, key[0:len(magic)]) { + return nil, errors.New("ssh: invalid openssh private key format") + } + remaining := key[len(magic):] + + var w struct { + CipherName string + KdfName string + KdfOpts string + NumKeys uint32 + PubKey []byte + PrivKeyBlock []byte + } + + if err := Unmarshal(remaining, &w); err != nil { + return nil, err + } + + pk1 := struct { + Check1 uint32 + Check2 uint32 + Keytype string + Pub []byte + Priv []byte + Comment string + Pad []byte `ssh:"rest"` + }{} + + if err := Unmarshal(w.PrivKeyBlock, &pk1); err != nil { + return nil, err + } + + if pk1.Check1 != pk1.Check2 { + return nil, errors.New("ssh: checkint mismatch") + } + + // we only handle ed25519 keys currently + if pk1.Keytype != KeyAlgoED25519 { + return nil, errors.New("ssh: unhandled key type") + } + + for i, b := range pk1.Pad { + if int(b) != i+1 { + return nil, errors.New("ssh: padding not as expected") + } + } + + if len(pk1.Priv) != ed25519.PrivateKeySize { + return nil, errors.New("ssh: private key unexpected length") + } + + pk := ed25519.PrivateKey(make([]byte, ed25519.PrivateKeySize)) + copy(pk, pk1.Priv) + return &pk, nil +} diff --git a/ssh/keys_test.go b/ssh/keys_test.go index 2756947..4c4c5be 100644 --- a/ssh/keys_test.go +++ b/ssh/keys_test.go @@ -17,6 +17,7 @@ import ( "strings" "testing" + "golang.org/x/crypto/ed25519" "golang.org/x/crypto/ssh/testdata" ) @@ -28,6 +29,8 @@ func rawKey(pub PublicKey) interface{} { return (*dsa.PublicKey)(k) case *ecdsaPublicKey: return (*ecdsa.PublicKey)(k) + case ed25519PublicKey: + return (ed25519.PublicKey)(k) case *Certificate: return k } diff --git a/ssh/server.go b/ssh/server.go index 9b29b15..e73a1c1 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -224,7 +224,7 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) func isAcceptableAlgo(algo string) bool { switch algo { - case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, + case KeyAlgoRSA, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoED25519, CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01: return true } diff --git a/ssh/test/session_test.go b/ssh/test/session_test.go index c0e714b..d27ade7 100644 --- a/ssh/test/session_test.go +++ b/ssh/test/session_test.go @@ -338,3 +338,28 @@ func TestKeyExchanges(t *testing.T) { } } } + +func TestClientAuthAlgorithms(t *testing.T) { + for _, key := range []string{ + "rsa", + "dsa", + "ecdsa", + "ed25519", + } { + server := newServer(t) + conf := clientConfig() + conf.SetDefaults() + conf.Auth = []ssh.AuthMethod{ + ssh.PublicKeys(testSigners[key]), + } + + conn, err := server.TryDial(conf) + if err == nil { + conn.Close() + } else { + t.Errorf("failed for key %q", key) + } + + server.Shutdown() + } +} diff --git a/ssh/test/test_unix_test.go b/ssh/test/test_unix_test.go index cded9b6..3bfd881 100644 --- a/ssh/test/test_unix_test.go +++ b/ssh/test/test_unix_test.go @@ -41,7 +41,7 @@ PermitRootLogin no StrictModes no RSAAuthentication yes PubkeyAuthentication yes -AuthorizedKeysFile {{.Dir}}/id_user.pub +AuthorizedKeysFile {{.Dir}}/authorized_keys TrustedUserCAKeys {{.Dir}}/id_ecdsa.pub IgnoreRhosts yes RhostsRSAAuthentication no @@ -250,6 +250,12 @@ func newServer(t *testing.T) *server { writeFile(filepath.Join(dir, filename+".pub"), ssh.MarshalAuthorizedKey(testPublicKeys[k])) } + var authkeys bytes.Buffer + for k, _ := range testdata.PEMBytes { + authkeys.Write(ssh.MarshalAuthorizedKey(testPublicKeys[k])) + } + writeFile(filepath.Join(dir, "authorized_keys"), authkeys.Bytes()) + return &server{ t: t, configfile: f.Name(), diff --git a/ssh/testdata/keys.go b/ssh/testdata/keys.go index cdf6619..9b76905 100644 --- a/ssh/testdata/keys.go +++ b/ssh/testdata/keys.go @@ -39,6 +39,14 @@ NDvRS0rjwt6lJGv7zPZoqDc65VfrK2aNyHx2PgFyzwrEOtuF57bu7pnvEIxpLTeM z26i6XVMeYXAWZMTloMCQBbpGgEERQpeUknLBqUHhg/wXF6+lFA+vEGnkY+Dwab2 KCXFGd+SQ5GdUcEMe9isUH6DYj/6/yCDoFrXXmpQb+M= -----END RSA PRIVATE KEY----- +`), + "ed25519": []byte(`-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACA+3f7hS7g5UWwXOGVTrMfhmxyrjqz7Sxxbx7I1j8DvvwAAAJhAFfkOQBX5 +DgAAAAtzc2gtZWQyNTUxOQAAACA+3f7hS7g5UWwXOGVTrMfhmxyrjqz7Sxxbx7I1j8Dvvw +AAAEAaYmXltfW6nhRo3iWGglRB48lYq0z0Q3I3KyrdutEr6j7d/uFLuDlRbBc4ZVOsx+Gb +HKuOrPtLHFvHsjWPwO+/AAAAE2dhcnRvbm1AZ2FydG9ubS14cHMBAg== +-----END OPENSSH PRIVATE KEY----- `), "user": []byte(`-----BEGIN EC PRIVATE KEY----- MHcCAQEEILYCAeq8f7V4vSSypRw7pxy8yz3V5W4qg8kSC3zJhqpQoAoGCCqGSM49