Explorar el Código

ssh: add functions for public keys in wire & auth keys format.

This allows easy import/export of public keys in the format
expected by OpenSSH for authorized_keys files, as well as
allowing comparisons with ServerConfig's PublickeyCallback.
Fixes golang/go#3908.

R=agl, dave, golang-dev, bradfitz
CC=agl, golang-dev
https://golang.org/cl/6855107
Shawn Ledbetter hace 13 años
padre
commit
887809b6be
Se han modificado 2 ficheros con 359 adiciones y 0 borrados
  1. 183 0
      ssh/keys.go
  2. 176 0
      ssh/test/keys_test.go

+ 183 - 0
ssh/keys.go

@@ -5,11 +5,22 @@
 package ssh
 
 import (
+	"bytes"
 	"crypto/dsa"
 	"crypto/rsa"
+	"encoding/base64"
 	"math/big"
 )
 
+// Key types supported by OpenSSH 5.9
+const (
+	keyAlgoRSA      = "ssh-rsa"
+	keyAlgoDSA      = "ssh-dss"
+	keyAlgoECDSA256 = "ecdsa-sha2-nistp256"
+	keyAlgoECDSA384 = "ecdsa-sha2-nistp384"
+	keyAlgoECDSA521 = "ecdsa-sha2-nistp521"
+)
+
 // parsePubKey parses a public key according to RFC 4253, section 6.6.
 func parsePubKey(in []byte) (out interface{}, rest []byte, ok bool) {
 	algo, in, ok := parseString(in)
@@ -118,3 +129,175 @@ func marshalPubDSA(key *dsa.PublicKey) []byte {
 
 	return ret
 }
+
+// parseAuthorizedKey parses a public key in OpenSSH authorized_keys format
+// (see sshd(8) manual page) once the options and key type fields have been
+// removed.
+func parseAuthorizedKey(in []byte) (out interface{}, comment string, ok bool) {
+	in = bytes.TrimSpace(in)
+
+	i := bytes.IndexAny(in, " \t")
+	if i == -1 {
+		i = len(in)
+	}
+	base64Key := in[:i]
+
+	key := make([]byte, base64.StdEncoding.DecodedLen(len(base64Key)))
+	n, err := base64.StdEncoding.Decode(key, base64Key)
+	if err != nil {
+		return
+	}
+	key = key[:n]
+	out, _, ok = parsePubKey(key)
+	if !ok {
+		return nil, "", false
+	}
+	comment = string(bytes.TrimSpace(in[i:]))
+	return
+}
+
+// ParseAuthorizedKeys parses a public key from an authorized_keys
+// file used in OpenSSH according to the sshd(8) manual page.
+func ParseAuthorizedKey(in []byte) (out interface{}, comment string, options []string, rest []byte, ok bool) {
+	for len(in) > 0 {
+		end := bytes.IndexByte(in, '\n')
+		if end != -1 {
+			rest = in[end+1:]
+			in = in[:end]
+		} else {
+			rest = nil
+		}
+
+		end = bytes.IndexByte(in, '\r')
+		if end != -1 {
+			in = in[:end]
+		}
+
+		in = bytes.TrimSpace(in)
+		if len(in) == 0 || in[0] == '#' {
+			in = rest
+			continue
+		}
+
+		i := bytes.IndexAny(in, " \t")
+		if i == -1 {
+			in = rest
+			continue
+		}
+
+		field := string(in[:i])
+		switch field {
+		case keyAlgoRSA, keyAlgoDSA:
+			out, comment, ok = parseAuthorizedKey(in[i:])
+			if ok {
+				return
+			}
+		case keyAlgoECDSA256, keyAlgoECDSA384, keyAlgoECDSA521:
+			// We don't support these keys.
+			in = rest
+			continue
+		case hostAlgoRSACertV01, hostAlgoDSACertV01,
+			hostAlgoECDSA256CertV01, hostAlgoECDSA384CertV01, hostAlgoECDSA521CertV01:
+			// We don't support these certificates.
+			in = rest
+			continue
+		}
+
+		// No key type recognised. Maybe there's an options field at
+		// the beginning.
+		var b byte
+		inQuote := false
+		var candidateOptions []string
+		optionStart := 0
+		for i, b = range in {
+			isEnd := !inQuote && (b == ' ' || b == '\t')
+			if (b == ',' && !inQuote) || isEnd {
+				if i-optionStart > 0 {
+					candidateOptions = append(candidateOptions, string(in[optionStart:i]))
+				}
+				optionStart = i + 1
+			}
+			if isEnd {
+				break
+			}
+			if b == '"' && (i == 0 || (i > 0 && in[i-1] != '\\')) {
+				inQuote = !inQuote
+			}
+		}
+		for i < len(in) && (in[i] == ' ' || in[i] == '\t') {
+			i++
+		}
+		if i == len(in) {
+			// Invalid line: unmatched quote
+			in = rest
+			continue
+		}
+
+		in = in[i:]
+		i = bytes.IndexAny(in, " \t")
+		if i == -1 {
+			in = rest
+			continue
+		}
+
+		field = string(in[:i])
+		switch field {
+		case keyAlgoRSA, keyAlgoDSA:
+			out, comment, ok = parseAuthorizedKey(in[i:])
+			if ok {
+				options = candidateOptions
+				return
+			}
+		}
+
+		in = rest
+		continue
+	}
+
+	return
+}
+
+// ParsePublicKey parses an SSH public key formatted for use in
+// the SSH wire protocol.
+func ParsePublicKey(in []byte) (out interface{}, rest []byte, ok bool) {
+	return parsePubKey(in)
+}
+
+// MarshalAuthorizedKey returns a byte stream suitable for inclusion
+// in an OpenSSH authorized_keys file following the format specified
+// in the sshd(8) manual page.
+func MarshalAuthorizedKey(key interface{}) []byte {
+	b := &bytes.Buffer{}
+	switch keyType := key.(type) {
+	case *rsa.PublicKey:
+		b.WriteString(hostAlgoRSA)
+	case *dsa.PublicKey:
+		b.WriteString(hostAlgoDSA)
+	case *OpenSSHCertV01:
+		switch keyType.Key.(type) {
+		case *rsa.PublicKey:
+			b.WriteString(hostAlgoRSACertV01)
+		case *dsa.PublicKey:
+			b.WriteString(hostAlgoDSACertV01)
+		default:
+			panic("unexpected key type")
+		}
+	default:
+		panic("unexpected key type")
+	}
+
+	b.WriteByte(' ')
+	e := base64.NewEncoder(base64.StdEncoding, b)
+	e.Write(serializePublickey(key))
+	e.Close()
+	b.WriteByte('\n')
+	return b.Bytes()
+}
+
+// MarshalPublicKey serializes a *rsa.PublicKey, *dsa.PublicKey or
+// *OpenSSHCertV01 for use in the SSH wire protocol. It can be used for
+// comparison with the pubkey argument of ServerConfig's PublicKeyCallback as
+// well as for generating an authorized_keys or host_keys file.
+func MarshalPublicKey(key interface{}) []byte {
+	return serializePublickey(key)
+}

+ 176 - 0
ssh/test/keys_test.go

@@ -0,0 +1,176 @@
+package test
+
+import (
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"reflect"
+	"strings"
+	"testing"
+
+	"code.google.com/p/go.crypto/ssh"
+)
+
+var (
+	validKey = `AAAAB3NzaC1yc2EAAAADAQABAAABAQDEX/dPu4PmtvgK3La9zioCEDrJ` +
+		`yUr6xEIK7Pr+rLgydcqWTU/kt7w7gKjOw4vvzgHfjKl09CWyvgb+y5dCiTk` +
+		`9MxI+erGNhs3pwaoS+EavAbawB7iEqYyTep3YaJK+4RJ4OX7ZlXMAIMrTL+` +
+		`UVrK89t56hCkFYaAgo3VY+z6rb/b3bDBYtE1Y2tS7C3au73aDgeb9psIrSV` +
+		`86ucKBTl5X62FnYiyGd++xCnLB6uLximM5OKXfLzJQNS/QyZyk12g3D8y69` +
+		`Xw1GzCSKX1u1+MQboyf0HJcG2ryUCLHdcDVppApyHx2OLq53hlkQ/yxdflD` +
+		`qCqAE4j+doagSsIfC1T2T`
+
+	authWithOptions = []string{
+		`# comments to ignore before any keys...`,
+		``,
+		`env="HOME=/home/root",no-port-forwarding ssh-rsa ` + validKey + ` user@host`,
+		`# comments to ignore, along with a blank line`,
+		``,
+		`env="HOME=/home/root2" ssh-rsa ` + validKey + ` user2@host2`,
+		``,
+		`# more comments, plus a invalid entry`,
+		`ssh-rsa data-that-will-not-parse user@host3`,
+	}
+
+	authOptions              = strings.Join(authWithOptions, "\n")
+	authWithCRLF             = strings.Join(authWithOptions, "\r\n")
+	authInvalid              = []byte(`ssh-rsa`)
+	authWithQuotedCommaInEnv = []byte(`env="HOME=/home/root,dir",no-port-forwarding ssh-rsa ` + validKey + `   user@host`)
+	authWithQuotedSpaceInEnv = []byte(`env="HOME=/home/root dir",no-port-forwarding ssh-rsa ` + validKey + ` user@host`)
+	authWithQuotedQuoteInEnv = []byte(`env="HOME=/home/\"root dir",no-port-forwarding` + "\t" + `ssh-rsa` + "\t" + validKey + `   user@host`)
+
+	authWithDoubleQuotedQuote = []byte(`no-port-forwarding,env="HOME=/home/ \"root dir\"" ssh-rsa ` + validKey + "\t" + `user@host`)
+	authWithInvalidSpace      = []byte(`env="HOME=/home/root dir", no-port-forwarding ssh-rsa ` + validKey + ` user@host
+#more to follow but still no valid keys`)
+	authWithMissingQuote = []byte(`env="HOME=/home/root,no-port-forwarding ssh-rsa ` + validKey + ` user@host
+env="HOME=/home/root",shared-control ssh-rsa ` + validKey + ` user@host`)
+)
+
+func TestMarshalParsePublicKey(t *testing.T) {
+	pub := getTestPublicKey(t)
+	authKeys := ssh.MarshalAuthorizedKey(pub)
+	actualFields := strings.Fields(string(authKeys))
+	if len(actualFields) == 0 {
+		t.Fatalf("failed authKeys: %v", authKeys)
+	}
+
+	// drop the comment
+	expectedFields := strings.Fields(keys["authorized_keys"])[0:2]
+
+	if !reflect.DeepEqual(actualFields, expectedFields) {
+		t.Errorf("got %v, expected %v", actualFields, expectedFields)
+	}
+
+	actPub, _, _, _, ok := ssh.ParseAuthorizedKey([]byte(keys["authorized_keys"]))
+	if !ok {
+		t.Fatalf("cannot parse %v", keys["authorized_keys"])
+	}
+	if !reflect.DeepEqual(actPub, pub) {
+		t.Errorf("got %v, expected %v", actPub, pub)
+	}
+}
+
+type authResult struct {
+	pubKey   interface{} //*rsa.PublicKey
+	options  []string
+	comments string
+	rest     string
+	ok       bool
+}
+
+func testAuthorizedKeys(t *testing.T, authKeys []byte, expected []authResult) {
+	rest := authKeys
+	var values []authResult
+	for len(rest) > 0 {
+		var r authResult
+		r.pubKey, r.comments, r.options, rest, r.ok = ssh.ParseAuthorizedKey(rest)
+		r.rest = string(rest)
+		values = append(values, r)
+	}
+
+	if !reflect.DeepEqual(values, expected) {
+		t.Errorf("got %q, expected %q", values, expected)
+	}
+
+}
+
+func getTestPublicKey(t *testing.T) *rsa.PublicKey {
+	block, _ := pem.Decode([]byte(testClientPrivateKey))
+	if block == nil {
+		t.Fatalf("pem.Decode: %v", testClientPrivateKey)
+	}
+	priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
+	if err != nil {
+		t.Fatalf("x509.ParsePKCS1PrivateKey: %v", err)
+	}
+
+	return &priv.PublicKey
+}
+
+func TestAuth(t *testing.T) {
+	pub := getTestPublicKey(t)
+	rest2 := strings.Join(authWithOptions[3:], "\n")
+	rest3 := strings.Join(authWithOptions[6:], "\n")
+	testAuthorizedKeys(t, []byte(authOptions), []authResult{
+		{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
+		{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
+		{nil, nil, "", "", false},
+	})
+}
+
+func TestAuthWithCRLF(t *testing.T) {
+	pub := getTestPublicKey(t)
+	rest2 := strings.Join(authWithOptions[3:], "\r\n")
+	rest3 := strings.Join(authWithOptions[6:], "\r\n")
+	testAuthorizedKeys(t, []byte(authWithCRLF), []authResult{
+		{pub, []string{`env="HOME=/home/root"`, "no-port-forwarding"}, "user@host", rest2, true},
+		{pub, []string{`env="HOME=/home/root2"`}, "user2@host2", rest3, true},
+		{nil, nil, "", "", false},
+	})
+}
+
+func TestAuthWithQuotedSpaceInEnv(t *testing.T) {
+	pub := getTestPublicKey(t)
+	testAuthorizedKeys(t, []byte(authWithQuotedSpaceInEnv), []authResult{
+		{pub, []string{`env="HOME=/home/root dir"`, "no-port-forwarding"}, "user@host", "", true},
+	})
+}
+
+func TestAuthWithQuotedCommaInEnv(t *testing.T) {
+	pub := getTestPublicKey(t)
+	testAuthorizedKeys(t, []byte(authWithQuotedCommaInEnv), []authResult{
+		{pub, []string{`env="HOME=/home/root,dir"`, "no-port-forwarding"}, "user@host", "", true},
+	})
+}
+
+func TestAuthWithQuotedQuoteInEnv(t *testing.T) {
+	pub := getTestPublicKey(t)
+	testAuthorizedKeys(t, []byte(authWithQuotedQuoteInEnv), []authResult{
+		{pub, []string{`env="HOME=/home/\"root dir"`, "no-port-forwarding"}, "user@host", "", true},
+	})
+
+	testAuthorizedKeys(t, []byte(authWithDoubleQuotedQuote), []authResult{
+		{pub, []string{"no-port-forwarding", `env="HOME=/home/ \"root dir\""`}, "user@host", "", true},
+	})
+
+}
+
+func TestAuthWithInvalidSpace(t *testing.T) {
+	testAuthorizedKeys(t, []byte(authWithInvalidSpace), []authResult{
+		{nil, nil, "", "", false},
+	})
+}
+
+func TestAuthWithMissingQuote(t *testing.T) {
+	pub := getTestPublicKey(t)
+	testAuthorizedKeys(t, []byte(authWithMissingQuote), []authResult{
+		{pub, []string{`env="HOME=/home/root"`, `shared-control`}, "user@host", "", true},
+	})
+}
+
+func TestInvalidEntry(t *testing.T) {
+	_, _, _, _, ok := ssh.ParseAuthorizedKey(authInvalid)
+	if ok {
+		t.Errorf("Expected invalid entry, returned valid entry")
+	}
+}