Jelajahi Sumber

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 <hanwen@google.com>
Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
Martin Garton 9 tahun lalu
induk
melakukan
1e61df8d9e
9 mengubah file dengan 178 tambahan dan 12 penghapusan
  1. 1 1
      ssh/agent/client.go
  2. 3 1
      ssh/certs.go
  3. 2 0
      ssh/common.go
  4. 128 8
      ssh/keys.go
  5. 3 0
      ssh/keys_test.go
  6. 1 1
      ssh/server.go
  7. 25 0
      ssh/test/session_test.go
  8. 7 1
      ssh/test/test_unix_test.go
  9. 8 0
      ssh/testdata/keys.go

+ 1 - 1
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)
 }

+ 3 - 1
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
 }

+ 2 - 0
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.

+ 128 - 8
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
+}

+ 3 - 0
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
 	}

+ 1 - 1
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
 	}

+ 25 - 0
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()
+	}
+}

+ 7 - 1
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(),

+ 8 - 0
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