github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/crypto/ssh/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 known_hosts
     6  // host key database, and provides utility functions for writing
     7  // OpenSSH compliant known_hosts files.
     8  package knownhosts
     9  
    10  import (
    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  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/crypto/ssh"
    25  )
    26  
    27  // See the sshd manpage
    28  // (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for
    29  // background.
    30  
    31  type addr struct{ host, port string }
    32  
    33  func (a *addr) String() string {
    34  	h := a.host
    35  	if strings.Contains(h, ":") {
    36  		h = "[" + h + "]"
    37  	}
    38  	return h + ":" + a.port
    39  }
    40  
    41  type matcher interface {
    42  	match(addr) bool
    43  }
    44  
    45  type hostPattern struct {
    46  	negate bool
    47  	addr   addr
    48  }
    49  
    50  func (p *hostPattern) String() string {
    51  	n := ""
    52  	if p.negate {
    53  		n = "!"
    54  	}
    55  
    56  	return n + p.addr.String()
    57  }
    58  
    59  type hostPatterns []hostPattern
    60  
    61  func (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
    78  func 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  
   109  func (p *hostPattern) match(a addr) bool {
   110  	return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port
   111  }
   112  
   113  type keyDBLine struct {
   114  	cert     bool
   115  	matcher  matcher
   116  	knownKey KnownKey
   117  }
   118  
   119  func serialize(k ssh.PublicKey) string {
   120  	return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
   121  }
   122  
   123  func (l *keyDBLine) match(a addr) bool {
   124  	return l.matcher.match(a)
   125  }
   126  
   127  type hostKeyDB struct {
   128  	// Serialized version of revoked keys
   129  	revoked map[string]*KnownKey
   130  	lines   []keyDBLine
   131  }
   132  
   133  func newHostKeyDB() *hostKeyDB {
   134  	db := &hostKeyDB{
   135  		revoked: make(map[string]*KnownKey),
   136  	}
   137  
   138  	return db
   139  }
   140  
   141  func keyEq(a, b ssh.PublicKey) bool {
   142  	return bytes.Equal(a.Marshal(), b.Marshal())
   143  }
   144  
   145  // IsAuthorityForHost can be used as a callback in ssh.CertChecker
   146  func (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
   162  func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool {
   163  	_, ok := db.revoked[string(key.Marshal())]
   164  	return ok
   165  }
   166  
   167  const markerCert = "@cert-authority"
   168  const markerRevoked = "@revoked"
   169  
   170  func 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  
   179  func 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  
   210  func (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  
   249  func 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.
   289  type KnownKey struct {
   290  	Key      ssh.PublicKey
   291  	Filename string
   292  	Line     int
   293  }
   294  
   295  func (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.
   303  type KeyError struct {
   304  	// Want holds the accepted host keys. For each key algorithm,
   305  	// there can be one hostkey.  If Want is empty, the host is
   306  	// unknown. If Want is non-empty, there was a mismatch, which
   307  	// can signify a MITM attack.
   308  	Want []KnownKey
   309  }
   310  
   311  func (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.
   319  type RevokedError struct {
   320  	Revoked KnownKey
   321  }
   322  
   323  func (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.
   329  func (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.
   356  func (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  	// Algorithm => key.
   362  	knownKeys := map[string]KnownKey{}
   363  	for _, l := range db.lines {
   364  		if l.match(a) {
   365  			typ := l.knownKey.Key.Type()
   366  			if _, ok := knownKeys[typ]; !ok {
   367  				knownKeys[typ] = l.knownKey
   368  			}
   369  		}
   370  	}
   371  
   372  	keyErr := &KeyError{}
   373  	for _, v := range knownKeys {
   374  		keyErr.Want = append(keyErr.Want, v)
   375  	}
   376  
   377  	// Unknown remote host.
   378  	if len(knownKeys) == 0 {
   379  		return keyErr
   380  	}
   381  
   382  	// If the remote host starts using a different, unknown key type, we
   383  	// also interpret that as a mismatch.
   384  	if known, ok := knownKeys[remoteKey.Type()]; !ok || !keyEq(known.Key, remoteKey) {
   385  		return keyErr
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  // The Read function parses file contents.
   392  func (db *hostKeyDB) Read(r io.Reader, filename string) error {
   393  	scanner := bufio.NewScanner(r)
   394  
   395  	lineNum := 0
   396  	for scanner.Scan() {
   397  		lineNum++
   398  		line := scanner.Bytes()
   399  		line = bytes.TrimSpace(line)
   400  		if len(line) == 0 || line[0] == '#' {
   401  			continue
   402  		}
   403  
   404  		if err := db.parseLine(line, filename, lineNum); err != nil {
   405  			return fmt.Errorf("knownhosts: %s:%d: %v", filename, lineNum, err)
   406  		}
   407  	}
   408  	return scanner.Err()
   409  }
   410  
   411  // New creates a host key callback from the given OpenSSH host key
   412  // files. The returned callback is for use in
   413  // ssh.ClientConfig.HostKeyCallback. By preference, the key check
   414  // operates on the hostname if available, i.e. if a server changes its
   415  // IP address, the host key check will still succeed, even though a
   416  // record of the new IP address is not available.
   417  func New(files ...string) (ssh.HostKeyCallback, error) {
   418  	db := newHostKeyDB()
   419  	for _, fn := range files {
   420  		f, err := os.Open(fn)
   421  		if err != nil {
   422  			return nil, err
   423  		}
   424  		defer f.Close()
   425  		if err := db.Read(f, fn); err != nil {
   426  			return nil, err
   427  		}
   428  	}
   429  
   430  	var certChecker ssh.CertChecker
   431  	certChecker.IsHostAuthority = db.IsHostAuthority
   432  	certChecker.IsRevoked = db.IsRevoked
   433  	certChecker.HostKeyFallback = db.check
   434  
   435  	return certChecker.CheckHostKey, nil
   436  }
   437  
   438  // Normalize normalizes an address into the form used in known_hosts
   439  func Normalize(address string) string {
   440  	host, port, err := net.SplitHostPort(address)
   441  	if err != nil {
   442  		host = address
   443  		port = "22"
   444  	}
   445  	entry := host
   446  	if port != "22" {
   447  		entry = "[" + entry + "]:" + port
   448  	} else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
   449  		entry = "[" + entry + "]"
   450  	}
   451  	return entry
   452  }
   453  
   454  // Line returns a line to add append to the known_hosts files.
   455  func Line(addresses []string, key ssh.PublicKey) string {
   456  	var trimmed []string
   457  	for _, a := range addresses {
   458  		trimmed = append(trimmed, Normalize(a))
   459  	}
   460  
   461  	return strings.Join(trimmed, ",") + " " + serialize(key)
   462  }
   463  
   464  // HashHostname hashes the given hostname. The hostname is not
   465  // normalized before hashing.
   466  func HashHostname(hostname string) string {
   467  	// TODO(hanwen): check if we can safely normalize this always.
   468  	salt := make([]byte, sha1.Size)
   469  
   470  	_, err := rand.Read(salt)
   471  	if err != nil {
   472  		panic(fmt.Sprintf("crypto/rand failure %v", err))
   473  	}
   474  
   475  	hash := hashHost(hostname, salt)
   476  	return encodeHash(sha1HashType, salt, hash)
   477  }
   478  
   479  func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
   480  	if len(encoded) == 0 || encoded[0] != '|' {
   481  		err = errors.New("knownhosts: hashed host must start with '|'")
   482  		return
   483  	}
   484  	components := strings.Split(encoded, "|")
   485  	if len(components) != 4 {
   486  		err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
   487  		return
   488  	}
   489  
   490  	hashType = components[1]
   491  	if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
   492  		return
   493  	}
   494  	if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
   495  		return
   496  	}
   497  	return
   498  }
   499  
   500  func encodeHash(typ string, salt []byte, hash []byte) string {
   501  	return strings.Join([]string{"",
   502  		typ,
   503  		base64.StdEncoding.EncodeToString(salt),
   504  		base64.StdEncoding.EncodeToString(hash),
   505  	}, "|")
   506  }
   507  
   508  // See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
   509  func hashHost(hostname string, salt []byte) []byte {
   510  	mac := hmac.New(sha1.New, salt)
   511  	mac.Write([]byte(hostname))
   512  	return mac.Sum(nil)
   513  }
   514  
   515  type hashedHost struct {
   516  	salt []byte
   517  	hash []byte
   518  }
   519  
   520  const sha1HashType = "1"
   521  
   522  func newHashedHost(encoded string) (*hashedHost, error) {
   523  	typ, salt, hash, err := decodeHash(encoded)
   524  	if err != nil {
   525  		return nil, err
   526  	}
   527  
   528  	// The type field seems for future algorithm agility, but it's
   529  	// actually hardcoded in openssh currently, see
   530  	// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
   531  	if typ != sha1HashType {
   532  		return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
   533  	}
   534  
   535  	return &hashedHost{salt: salt, hash: hash}, nil
   536  }
   537  
   538  func (h *hashedHost) match(a addr) bool {
   539  	return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
   540  }