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