ssh/knownhosts: support hashed hostnames

Change-Id: I855a6542a2eb2ae1d223f03892c0f19da81a4f8d
Reviewed-on: https://go-review.googlesource.com/40532
Run-TryBot: Han-Wen Nienhuys <hanwen@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Adam Langley <agl@golang.org>
This commit is contained in:
Han-Wen Nienhuys
2017-04-20 11:20:44 +00:00
parent 12c985af85
commit 64205593f2
2 changed files with 231 additions and 83 deletions
+161 -83
View File
@@ -9,6 +9,9 @@ package knownhosts
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
@@ -27,11 +30,15 @@ import (
type addr struct{ host, port string }
func (a *addr) String() string {
return a.host + ":" + a.port
h := a.host
if strings.Contains(h, ":") {
h = "[" + h + "]"
}
return h + ":" + a.port
}
func (a *addr) eq(b addr) bool {
return a.host == b.host && a.port == b.port
type matcher interface {
match([]addr) bool
}
type hostPattern struct {
@@ -48,6 +55,25 @@ func (p *hostPattern) String() string {
return n + p.addr.String()
}
type hostPatterns []hostPattern
func (ps hostPatterns) match(addrs []addr) bool {
matched := false
for _, p := range ps {
for _, a := range addrs {
m := p.match(a)
if !m {
continue
}
if p.negate {
return false
}
matched = true
}
}
return matched
}
// See
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/addrmatch.c
// The matching of * has no regard for separators, unlike filesystem globs
@@ -88,48 +114,16 @@ func (l *hostPattern) match(a addr) bool {
type keyDBLine struct {
cert bool
patterns []*hostPattern
matcher matcher
knownKey KnownKey
}
func (l *keyDBLine) String() string {
c := ""
if l.cert {
c = markerCert + " "
}
var ss []string
for _, p := range l.patterns {
ss = append(ss, p.String())
}
return c + strings.Join(ss, ",") + " " + serialize(l.knownKey.Key)
}
func serialize(k ssh.PublicKey) string {
return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
}
func (l *keyDBLine) match(addrs []addr) bool {
matched := false
for _, p := range l.patterns {
for _, a := range addrs {
m := p.match(a)
if p.negate {
if m {
return false
} else {
continue
}
}
if m {
matched = true
}
}
}
return matched
return l.matcher.match(addrs)
}
type hostKeyDB struct {
@@ -138,17 +132,6 @@ type hostKeyDB struct {
lines []keyDBLine
}
func (db *hostKeyDB) String() string {
var ls []string
for _, k := range db.revoked {
ls = append(ls, markerRevoked+" * "+serialize(k.Key))
}
for _, l := range db.lines {
ls = append(ls, l.String())
}
return strings.Join(ls, "\n")
}
func newHostKeyDB() *hostKeyDB {
db := &hostKeyDB{
revoked: make(map[string]*KnownKey),
@@ -190,45 +173,39 @@ func nextWord(line []byte) (string, []byte) {
return string(line[:i]), bytes.TrimSpace(line[i:])
}
func parseLine(line []byte) (marker string, pattern []string, key ssh.PublicKey, err error) {
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
if w, next := nextWord(line); w == markerCert || w == markerRevoked {
marker = w
line = next
}
hostPart, line := nextWord(line)
host, line = nextWord(line)
if len(line) == 0 {
return "", nil, nil, errors.New("knownhosts: missing host pattern")
return "", "", nil, errors.New("knownhosts: missing host pattern")
}
if len(hostPart) > 0 && hostPart[0] == '|' {
return "", nil, nil, errors.New("knownhosts: hashed hostnames not implemented")
}
pattern = strings.Split(hostPart, ",")
// ignore the keytype as it's in the key blob anyway.
_, line = nextWord(line)
if len(line) == 0 {
return "", nil, nil, errors.New("knownhosts: missing key type pattern")
return "", "", nil, errors.New("knownhosts: missing key type pattern")
}
keyBlob, _ := nextWord(line)
keyBytes, err := base64.StdEncoding.DecodeString(keyBlob)
if err != nil {
return "", nil, nil, err
return "", "", nil, err
}
key, err = ssh.ParsePublicKey(keyBytes)
if err != nil {
return "", nil, nil, err
return "", "", nil, err
}
return marker, pattern, key, nil
return marker, host, key, nil
}
func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error {
marker, patterns, key, err := parseLine(line)
marker, pattern, key, err := parseLine(line)
if err != nil {
return err
}
@@ -252,7 +229,23 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
},
}
for _, p := range patterns {
if pattern[0] == '|' {
entry.matcher, err = newHashedHost(pattern)
} else {
entry.matcher, err = newHostnameMatcher(pattern)
}
if err != nil {
return err
}
db.lines = append(db.lines, entry)
return nil
}
func newHostnameMatcher(pattern string) (matcher, error) {
var hps hostPatterns
for _, p := range strings.Split(pattern, ",") {
if len(p) == 0 {
continue
}
@@ -265,13 +258,14 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
}
if len(p) == 0 {
return errors.New("knownhosts: negation without following hostname")
return nil, errors.New("knownhosts: negation without following hostname")
}
var err error
if p[0] == '[' {
a.host, a.port, err = net.SplitHostPort(p)
if err != nil {
return err
return nil, err
}
} else {
a.host, a.port, err = net.SplitHostPort(p)
@@ -280,15 +274,12 @@ func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error
a.port = "22"
}
}
entry.patterns = append(entry.patterns, &hostPattern{
hps = append(hps, hostPattern{
negate: negate,
addr: a,
})
}
db.lines = append(db.lines, entry)
return nil
return hps, nil
}
// KnownKey represents a key declared in a known_hosts file.
@@ -446,24 +437,111 @@ func New(files ...string) (ssh.HostKeyCallback, error) {
return certChecker.CheckHostKey, nil
}
// Normalize normalizes an address into the form used in known_hosts
func Normalize(address string) string {
host, port, err := net.SplitHostPort(address)
if err != nil {
host = address
port = "22"
}
entry := host
if port != "22" {
entry = "[" + entry + "]:" + port
} else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
entry = "[" + entry + "]"
}
return entry
}
// Line returns a line to add append to the known_hosts files.
func Line(addresses []string, key ssh.PublicKey) string {
var trimmed []string
for _, a := range addresses {
host, port, err := net.SplitHostPort(a)
if err != nil {
host = a
port = "22"
}
entry := host
if port != "22" {
entry = "[" + entry + "]:" + port
} else if strings.Contains(host, ":") {
entry = "[" + entry + "]"
}
trimmed = append(trimmed, entry)
trimmed = append(trimmed, Normalize(a))
}
return strings.Join(trimmed, ",") + " " + serialize(key)
}
// HashHostname hashes the given hostname. The hostname is not
// normalized before hashing.
func HashHostname(hostname string) string {
// TODO(hanwen): check if we can safely normalize this always.
salt := make([]byte, sha1.Size)
_, err := rand.Read(salt)
if err != nil {
panic(fmt.Sprintf("crypto/rand failure %v", err))
}
hash := hashHost(hostname, salt)
return encodeHash(sha1HashType, salt, hash)
}
func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
if len(encoded) == 0 || encoded[0] != '|' {
err = errors.New("knownhosts: hashed host must start with '|'")
return
}
components := strings.Split(encoded, "|")
if len(components) != 4 {
err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
return
}
hashType = components[1]
if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
return
}
if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
return
}
return
}
func encodeHash(typ string, salt []byte, hash []byte) string {
return strings.Join([]string{"",
typ,
base64.StdEncoding.EncodeToString(salt),
base64.StdEncoding.EncodeToString(hash),
}, "|")
}
// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
func hashHost(hostname string, salt []byte) []byte {
mac := hmac.New(sha1.New, salt)
mac.Write([]byte(hostname))
return mac.Sum(nil)
}
type hashedHost struct {
salt []byte
hash []byte
}
const sha1HashType = "1"
func newHashedHost(encoded string) (*hashedHost, error) {
typ, salt, hash, err := decodeHash(encoded)
if err != nil {
return nil, err
}
// The type field seems for future algorithm agility, but it's
// actually hardcoded in openssh currently, see
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
if typ != sha1HashType {
return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
}
return &hashedHost{salt: salt, hash: hash}, nil
}
func (h *hashedHost) match(addrs []addr) bool {
for _, a := range addrs {
if bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash) {
return true
}
}
return false
}
+70
View File
@@ -235,3 +235,73 @@ func TestWildcardMatch(t *testing.T) {
}
// TODO(hanwen): test coverage for certificates.
const testHostname = "hostname"
// generated with keygen -H -f
const encodedTestHostnameHash = "|1|IHXZvQMvTcZTUU29+2vXFgx8Frs=|UGccIWfRVDwilMBnA3WJoRAC75Y="
func TestHostHash(t *testing.T) {
testHostHash(t, testHostname, encodedTestHostnameHash)
}
func TestHashList(t *testing.T) {
encoded := HashHostname(testHostname)
testHostHash(t, testHostname, encoded)
}
func testHostHash(t *testing.T, hostname, encoded string) {
typ, salt, hash, err := decodeHash(encoded)
if err != nil {
t.Fatalf("decodeHash: %v", err)
}
if got := encodeHash(typ, salt, hash); got != encoded {
t.Errorf("got encoding %s want %s", got, encoded)
}
if typ != sha1HashType {
t.Fatalf("got hash type %q, want %q", typ, sha1HashType)
}
got := hashHost(hostname, salt)
if !bytes.Equal(got, hash) {
t.Errorf("got hash %x want %x", got, hash)
}
}
func TestNormalize(t *testing.T) {
for in, want := range map[string]string{
"127.0.0.1:22": "127.0.0.1",
"[127.0.0.1]:22": "127.0.0.1",
"[127.0.0.1]:23": "[127.0.0.1]:23",
"127.0.0.1:23": "[127.0.0.1]:23",
"[a.b.c]:22": "a.b.c",
"[abcd:abcd:abcd:abcd]": "[abcd:abcd:abcd:abcd]",
"[abcd:abcd:abcd:abcd]:22": "[abcd:abcd:abcd:abcd]",
"[abcd:abcd:abcd:abcd]:23": "[abcd:abcd:abcd:abcd]:23",
} {
got := Normalize(in)
if got != want {
t.Errorf("Normalize(%q) = %q, want %q", in, got, want)
}
}
}
func TestHashedHostkeyCheck(t *testing.T) {
str := fmt.Sprintf("%s %s", HashHostname(testHostname), edKeyStr)
db := testDB(t, str)
if err := db.check(testHostname+":22", testAddr, edKey); err != nil {
t.Errorf("check(%s): %v", testHostname, err)
}
want := &KeyError{
Want: []KnownKey{{
Filename: "testdb",
Line: 1,
Key: edKey,
}},
}
if got := db.check(testHostname+":22", testAddr, alternateEdKey); !reflect.DeepEqual(got, want) {
t.Errorf("got error %v, want %v", got, want)
}
}