main
Raw Download raw file
  1// Copyright 2017 The Go Authors. All rights reserved.
  2// Use of this source code is governed by a BSD-style
  3// license that can be found in the LICENSE file.
  4
  5// Package knownhosts implements a parser for the OpenSSH known_hosts
  6// host key database, and provides utility functions for writing
  7// OpenSSH compliant known_hosts files.
  8package knownhosts
  9
 10import (
 11	"bufio"
 12	"bytes"
 13	"crypto/hmac"
 14	"crypto/rand"
 15	"crypto/sha1"
 16	"encoding/base64"
 17	"errors"
 18	"fmt"
 19	"io"
 20	"net"
 21	"os"
 22	"strings"
 23
 24	"golang.org/x/crypto/ssh"
 25)
 26
 27// See the sshd manpage
 28// (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for
 29// background.
 30
 31type addr struct{ host, port string }
 32
 33func (a *addr) String() string {
 34	h := a.host
 35	if strings.Contains(h, ":") {
 36		h = "[" + h + "]"
 37	}
 38	return h + ":" + a.port
 39}
 40
 41type matcher interface {
 42	match(addr) bool
 43}
 44
 45type hostPattern struct {
 46	negate bool
 47	addr   addr
 48}
 49
 50func (p *hostPattern) String() string {
 51	n := ""
 52	if p.negate {
 53		n = "!"
 54	}
 55
 56	return n + p.addr.String()
 57}
 58
 59type hostPatterns []hostPattern
 60
 61func (ps hostPatterns) match(a addr) bool {
 62	matched := false
 63	for _, p := range ps {
 64		if !p.match(a) {
 65			continue
 66		}
 67		if p.negate {
 68			return false
 69		}
 70		matched = true
 71	}
 72	return matched
 73}
 74
 75// See
 76// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/addrmatch.c
 77// The matching of * has no regard for separators, unlike filesystem globs
 78func wildcardMatch(pat []byte, str []byte) bool {
 79	for {
 80		if len(pat) == 0 {
 81			return len(str) == 0
 82		}
 83		if len(str) == 0 {
 84			return false
 85		}
 86
 87		if pat[0] == '*' {
 88			if len(pat) == 1 {
 89				return true
 90			}
 91
 92			for j := range str {
 93				if wildcardMatch(pat[1:], str[j:]) {
 94					return true
 95				}
 96			}
 97			return false
 98		}
 99
100		if pat[0] == '?' || pat[0] == str[0] {
101			pat = pat[1:]
102			str = str[1:]
103		} else {
104			return false
105		}
106	}
107}
108
109func (p *hostPattern) match(a addr) bool {
110	return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port
111}
112
113type keyDBLine struct {
114	cert     bool
115	matcher  matcher
116	knownKey KnownKey
117}
118
119func serialize(k ssh.PublicKey) string {
120	return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
121}
122
123func (l *keyDBLine) match(a addr) bool {
124	return l.matcher.match(a)
125}
126
127type hostKeyDB struct {
128	// Serialized version of revoked keys
129	revoked map[string]*KnownKey
130	lines   []keyDBLine
131}
132
133func newHostKeyDB() *hostKeyDB {
134	db := &hostKeyDB{
135		revoked: make(map[string]*KnownKey),
136	}
137
138	return db
139}
140
141func keyEq(a, b ssh.PublicKey) bool {
142	return bytes.Equal(a.Marshal(), b.Marshal())
143}
144
145// IsHostAuthority can be used as a callback in ssh.CertChecker
146func (db *hostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
147	h, p, err := net.SplitHostPort(address)
148	if err != nil {
149		return false
150	}
151	a := addr{host: h, port: p}
152
153	for _, l := range db.lines {
154		if l.cert && keyEq(l.knownKey.Key, remote) && l.match(a) {
155			return true
156		}
157	}
158	return false
159}
160
161// IsRevoked can be used as a callback in ssh.CertChecker
162func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
163	_, ok := db.revoked[string(key.Marshal())]
164	return ok
165}
166
167const markerCert = "@cert-authority"
168const markerRevoked = "@revoked"
169
170func nextWord(line []byte) (string, []byte) {
171	i := bytes.IndexAny(line, "\t ")
172	if i == -1 {
173		return string(line), nil
174	}
175
176	return string(line[:i]), bytes.TrimSpace(line[i:])
177}
178
179func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
180	if w, next := nextWord(line); w == markerCert || w == markerRevoked {
181		marker = w
182		line = next
183	}
184
185	host, line = nextWord(line)
186	if len(line) == 0 {
187		return "", "", nil, errors.New("knownhosts: missing host pattern")
188	}
189
190	// ignore the keytype as it's in the key blob anyway.
191	_, line = nextWord(line)
192	if len(line) == 0 {
193		return "", "", nil, errors.New("knownhosts: missing key type pattern")
194	}
195
196	keyBlob, _ := nextWord(line)
197
198	keyBytes, err := base64.StdEncoding.DecodeString(keyBlob)
199	if err != nil {
200		return "", "", nil, err
201	}
202	key, err = ssh.ParsePublicKey(keyBytes)
203	if err != nil {
204		return "", "", nil, err
205	}
206
207	return marker, host, key, nil
208}
209
210func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error {
211	marker, pattern, key, err := parseLine(line)
212	if err != nil {
213		return err
214	}
215
216	if marker == markerRevoked {
217		db.revoked[string(key.Marshal())] = &KnownKey{
218			Key:      key,
219			Filename: filename,
220			Line:     linenum,
221		}
222
223		return nil
224	}
225
226	entry := keyDBLine{
227		cert: marker == markerCert,
228		knownKey: KnownKey{
229			Filename: filename,
230			Line:     linenum,
231			Key:      key,
232		},
233	}
234
235	if pattern[0] == '|' {
236		entry.matcher, err = newHashedHost(pattern)
237	} else {
238		entry.matcher, err = newHostnameMatcher(pattern)
239	}
240
241	if err != nil {
242		return err
243	}
244
245	db.lines = append(db.lines, entry)
246	return nil
247}
248
249func newHostnameMatcher(pattern string) (matcher, error) {
250	var hps hostPatterns
251	for _, p := range strings.Split(pattern, ",") {
252		if len(p) == 0 {
253			continue
254		}
255
256		var a addr
257		var negate bool
258		if p[0] == '!' {
259			negate = true
260			p = p[1:]
261		}
262
263		if len(p) == 0 {
264			return nil, errors.New("knownhosts: negation without following hostname")
265		}
266
267		var err error
268		if p[0] == '[' {
269			a.host, a.port, err = net.SplitHostPort(p)
270			if err != nil {
271				return nil, err
272			}
273		} else {
274			a.host, a.port, err = net.SplitHostPort(p)
275			if err != nil {
276				a.host = p
277				a.port = "22"
278			}
279		}
280		hps = append(hps, hostPattern{
281			negate: negate,
282			addr:   a,
283		})
284	}
285	return hps, nil
286}
287
288// KnownKey represents a key declared in a known_hosts file.
289type KnownKey struct {
290	Key      ssh.PublicKey
291	Filename string
292	Line     int
293}
294
295func (k *KnownKey) String() string {
296	return fmt.Sprintf("%s:%d: %s", k.Filename, k.Line, serialize(k.Key))
297}
298
299// KeyError is returned if we did not find the key in the host key
300// database, or there was a mismatch.  Typically, in batch
301// applications, this should be interpreted as failure. Interactive
302// applications can offer an interactive prompt to the user.
303type KeyError struct {
304	// Want holds the accepted host keys. For each key algorithm,
305	// there can be multiple hostkeys.  If Want is empty, the host
306	// is unknown. If Want is non-empty, there was a mismatch, which
307	// can signify a MITM attack.
308	Want []KnownKey
309}
310
311func (u *KeyError) Error() string {
312	if len(u.Want) == 0 {
313		return "knownhosts: key is unknown"
314	}
315	return "knownhosts: key mismatch"
316}
317
318// RevokedError is returned if we found a key that was revoked.
319type RevokedError struct {
320	Revoked KnownKey
321}
322
323func (r *RevokedError) Error() string {
324	return "knownhosts: key is revoked"
325}
326
327// check checks a key against the host database. This should not be
328// used for verifying certificates.
329func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
330	if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
331		return &RevokedError{Revoked: *revoked}
332	}
333
334	host, port, err := net.SplitHostPort(remote.String())
335	if err != nil {
336		return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err)
337	}
338
339	hostToCheck := addr{host, port}
340	if address != "" {
341		// Give preference to the hostname if available.
342		host, port, err := net.SplitHostPort(address)
343		if err != nil {
344			return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err)
345		}
346
347		hostToCheck = addr{host, port}
348	}
349
350	return db.checkAddr(hostToCheck, remoteKey)
351}
352
353// checkAddr checks if we can find the given public key for the
354// given address.  If we only find an entry for the IP address,
355// or only the hostname, then this still succeeds.
356func (db *hostKeyDB) checkAddr(a addr, remoteKey ssh.PublicKey) error {
357	// TODO(hanwen): are these the right semantics? What if there
358	// is just a key for the IP address, but not for the
359	// hostname?
360
361	keyErr := &KeyError{}
362
363	for _, l := range db.lines {
364		if !l.match(a) {
365			continue
366		}
367
368		keyErr.Want = append(keyErr.Want, l.knownKey)
369		if keyEq(l.knownKey.Key, remoteKey) {
370			return nil
371		}
372	}
373
374	return keyErr
375}
376
377// The Read function parses file contents.
378func (db *hostKeyDB) Read(r io.Reader, filename string) error {
379	scanner := bufio.NewScanner(r)
380
381	lineNum := 0
382	for scanner.Scan() {
383		lineNum++
384		line := scanner.Bytes()
385		line = bytes.TrimSpace(line)
386		if len(line) == 0 || line[0] == '#' {
387			continue
388		}
389
390		if err := db.parseLine(line, filename, lineNum); err != nil {
391			return fmt.Errorf("knownhosts: %s:%d: %v", filename, lineNum, err)
392		}
393	}
394	return scanner.Err()
395}
396
397// New creates a host key callback from the given OpenSSH host key
398// files. The returned callback is for use in
399// ssh.ClientConfig.HostKeyCallback. By preference, the key check
400// operates on the hostname if available, i.e. if a server changes its
401// IP address, the host key check will still succeed, even though a
402// record of the new IP address is not available.
403func New(files ...string) (ssh.HostKeyCallback, error) {
404	db := newHostKeyDB()
405	for _, fn := range files {
406		f, err := os.Open(fn)
407		if err != nil {
408			return nil, err
409		}
410		defer f.Close()
411		if err := db.Read(f, fn); err != nil {
412			return nil, err
413		}
414	}
415
416	var certChecker ssh.CertChecker
417	certChecker.IsHostAuthority = db.IsHostAuthority
418	certChecker.IsRevoked = db.IsRevoked
419	certChecker.HostKeyFallback = db.check
420
421	return certChecker.CheckHostKey, nil
422}
423
424// Normalize normalizes an address into the form used in known_hosts
425func Normalize(address string) string {
426	host, port, err := net.SplitHostPort(address)
427	if err != nil {
428		host = address
429		port = "22"
430	}
431	entry := host
432	if port != "22" {
433		entry = "[" + entry + "]:" + port
434	} else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
435		entry = "[" + entry + "]"
436	}
437	return entry
438}
439
440// Line returns a line to add append to the known_hosts files.
441func Line(addresses []string, key ssh.PublicKey) string {
442	var trimmed []string
443	for _, a := range addresses {
444		trimmed = append(trimmed, Normalize(a))
445	}
446
447	return strings.Join(trimmed, ",") + " " + serialize(key)
448}
449
450// HashHostname hashes the given hostname. The hostname is not
451// normalized before hashing.
452func HashHostname(hostname string) string {
453	// TODO(hanwen): check if we can safely normalize this always.
454	salt := make([]byte, sha1.Size)
455
456	_, err := rand.Read(salt)
457	if err != nil {
458		panic(fmt.Sprintf("crypto/rand failure %v", err))
459	}
460
461	hash := hashHost(hostname, salt)
462	return encodeHash(sha1HashType, salt, hash)
463}
464
465func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
466	if len(encoded) == 0 || encoded[0] != '|' {
467		err = errors.New("knownhosts: hashed host must start with '|'")
468		return
469	}
470	components := strings.Split(encoded, "|")
471	if len(components) != 4 {
472		err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
473		return
474	}
475
476	hashType = components[1]
477	if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
478		return
479	}
480	if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
481		return
482	}
483	return
484}
485
486func encodeHash(typ string, salt []byte, hash []byte) string {
487	return strings.Join([]string{"",
488		typ,
489		base64.StdEncoding.EncodeToString(salt),
490		base64.StdEncoding.EncodeToString(hash),
491	}, "|")
492}
493
494// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
495func hashHost(hostname string, salt []byte) []byte {
496	mac := hmac.New(sha1.New, salt)
497	mac.Write([]byte(hostname))
498	return mac.Sum(nil)
499}
500
501type hashedHost struct {
502	salt []byte
503	hash []byte
504}
505
506const sha1HashType = "1"
507
508func newHashedHost(encoded string) (*hashedHost, error) {
509	typ, salt, hash, err := decodeHash(encoded)
510	if err != nil {
511		return nil, err
512	}
513
514	// The type field seems for future algorithm agility, but it's
515	// actually hardcoded in openssh currently, see
516	// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
517	if typ != sha1HashType {
518		return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
519	}
520
521	return &hashedHost{salt: salt, hash: hash}, nil
522}
523
524func (h *hashedHost) match(a addr) bool {
525	return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
526}