Skip to content

Commit

Permalink
x/crypto/ssh: add support for ed25519 keys
Browse files Browse the repository at this point in the history
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 <[email protected]>
Run-TryBot: Han-Wen Nienhuys <[email protected]>
  • Loading branch information
mjgarton authored and hanwen committed May 12, 2016
1 parent b76c864 commit 1e61df8
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 12 deletions.
2 changes: 1 addition & 1 deletion ssh/agent/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
4 changes: 3 additions & 1 deletion ssh/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
CertAlgoECDSA256v01 = "[email protected]"
CertAlgoECDSA384v01 = "[email protected]"
CertAlgoECDSA521v01 = "[email protected]"
CertAlgoED25519v01 = "[email protected]"
)

// Certificate types distinguish between host and user
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 2 additions & 0 deletions ssh/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var supportedHostKeyAlgos = []string{

KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521,
KeyAlgoRSA, KeyAlgoDSA,

KeyAlgoED25519,
}

// supportedMACs specifies a default set of MAC algorithms in preference order.
Expand Down
136 changes: 128 additions & 8 deletions ssh/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 changes: 3 additions & 0 deletions ssh/keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"
"testing"

"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh/testdata"
)

Expand All @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
25 changes: 25 additions & 0 deletions ssh/test/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
8 changes: 7 additions & 1 deletion ssh/test/test_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down
8 changes: 8 additions & 0 deletions ssh/testdata/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1e61df8

Please sign in to comment.