github.com/glycerine/xcryptossh@v7.0.4+incompatible/knownhosts/knownhosts.go (about)

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