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
This commit is contained in:
Родитель
62944567d8
Коммит
887809b6be
183
ssh/keys.go
183
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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче