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.
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.
24 "golang.org/x/crypto/ssh"
27 // See the sshd manpage
28 // (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for
31 type addr struct{ host, port string }
33 func (a *addr) String() string {
35 if strings.Contains(h, ":") {
38 return h + ":" + a.port
41 type matcher interface {
45 type hostPattern struct {
50 func (p *hostPattern) String() string {
56 return n + p.addr.String()
59 type hostPatterns []hostPattern
61 func (ps hostPatterns) match(a addr) bool {
63 for _, p := range ps {
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 {
93 if wildcardMatch(pat[1:], str[j:]) {
100 if pat[0] == '?' || pat[0] == str[0] {
109 func (p *hostPattern) match(a addr) bool {
110 return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port
113 type keyDBLine struct {
119 func serialize(k ssh.PublicKey) string {
120 return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal())
123 func (l *keyDBLine) match(a addr) bool {
124 return l.matcher.match(a)
127 type hostKeyDB struct {
128 // Serialized version of revoked keys
129 revoked map[string]*KnownKey
133 func newHostKeyDB() *hostKeyDB {
135 revoked: make(map[string]*KnownKey),
141 func keyEq(a, b ssh.PublicKey) bool {
142 return bytes.Equal(a.Marshal(), b.Marshal())
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)
151 a := addr{host: h, port: p}
153 for _, l := range db.lines {
154 if l.cert && keyEq(l.knownKey.Key, remote) && l.match(a) {
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())]
167 const markerCert = "@cert-authority"
168 const markerRevoked = "@revoked"
170 func nextWord(line []byte) (string, []byte) {
171 i := bytes.IndexAny(line, "\t ")
173 return string(line), nil
176 return string(line[:i]), bytes.TrimSpace(line[i:])
179 func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
180 if w, next := nextWord(line); w == markerCert || w == markerRevoked {
185 host, line = nextWord(line)
187 return "", "", nil, errors.New("knownhosts: missing host pattern")
190 // ignore the keytype as it's in the key blob anyway.
191 _, line = nextWord(line)
193 return "", "", nil, errors.New("knownhosts: missing key type pattern")
196 keyBlob, _ := nextWord(line)
198 keyBytes, err := base64.StdEncoding.DecodeString(keyBlob)
200 return "", "", nil, err
202 key, err = ssh.ParsePublicKey(keyBytes)
204 return "", "", nil, err
207 return marker, host, key, nil
210 func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error {
211 marker, pattern, key, err := parseLine(line)
216 if marker == markerRevoked {
217 db.revoked[string(key.Marshal())] = &KnownKey{
227 cert: marker == markerCert,
235 if pattern[0] == '|' {
236 entry.matcher, err = newHashedHost(pattern)
238 entry.matcher, err = newHostnameMatcher(pattern)
245 db.lines = append(db.lines, entry)
249 func newHostnameMatcher(pattern string) (matcher, error) {
251 for _, p := range strings.Split(pattern, ",") {
264 return nil, errors.New("knownhosts: negation without following hostname")
269 a.host, a.port, err = net.SplitHostPort(p)
274 a.host, a.port, err = net.SplitHostPort(p)
280 hps = append(hps, hostPattern{
288 // KnownKey represents a key declared in a known_hosts file.
289 type KnownKey struct {
295 func (k *KnownKey) String() string {
296 return fmt.Sprintf("%s:%d: %s", k.Filename, k.Line, serialize(k.Key))
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.
311 func (u *KeyError) Error() string {
312 if len(u.Want) == 0 {
313 return "knownhosts: key is unknown"
315 return "knownhosts: key mismatch"
318 // RevokedError is returned if we found a key that was revoked.
319 type RevokedError struct {
323 func (r *RevokedError) Error() string {
324 return "knownhosts: key is revoked"
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}
334 host, port, err := net.SplitHostPort(remote.String())
336 return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err)
339 hostToCheck := addr{host, port}
341 // Give preference to the hostname if available.
342 host, port, err := net.SplitHostPort(address)
344 return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err)
347 hostToCheck = addr{host, port}
350 return db.checkAddr(hostToCheck, remoteKey)
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
362 knownKeys := map[string]KnownKey{}
363 for _, l := range db.lines {
365 typ := l.knownKey.Key.Type()
366 if _, ok := knownKeys[typ]; !ok {
367 knownKeys[typ] = l.knownKey
372 keyErr := &KeyError{}
373 for _, v := range knownKeys {
374 keyErr.Want = append(keyErr.Want, v)
377 // Unknown remote host.
378 if len(knownKeys) == 0 {
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) {
391 // The Read function parses file contents.
392 func (db *hostKeyDB) Read(r io.Reader, filename string) error {
393 scanner := bufio.NewScanner(r)
398 line := scanner.Bytes()
399 line = bytes.TrimSpace(line)
400 if len(line) == 0 || line[0] == '#' {
404 if err := db.parseLine(line, filename, lineNum); err != nil {
405 return fmt.Errorf("knownhosts: %s:%d: %v", filename, lineNum, err)
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) {
419 for _, fn := range files {
420 f, err := os.Open(fn)
425 if err := db.Read(f, fn); err != nil {
430 var certChecker ssh.CertChecker
431 certChecker.IsHostAuthority = db.IsHostAuthority
432 certChecker.IsRevoked = db.IsRevoked
433 certChecker.HostKeyFallback = db.check
435 return certChecker.CheckHostKey, nil
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)
447 entry = "[" + entry + "]:" + port
448 } else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") {
449 entry = "[" + entry + "]"
454 // Line returns a line to add append to the known_hosts files.
455 func Line(addresses []string, key ssh.PublicKey) string {
457 for _, a := range addresses {
458 trimmed = append(trimmed, Normalize(a))
461 return strings.Join(trimmed, ",") + " " + serialize(key)
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)
470 _, err := rand.Read(salt)
472 panic(fmt.Sprintf("crypto/rand failure %v", err))
475 hash := hashHost(hostname, salt)
476 return encodeHash(sha1HashType, salt, hash)
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 '|'")
484 components := strings.Split(encoded, "|")
485 if len(components) != 4 {
486 err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
490 hashType = components[1]
491 if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
494 if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
500 func encodeHash(typ string, salt []byte, hash []byte) string {
501 return strings.Join([]string{"",
503 base64.StdEncoding.EncodeToString(salt),
504 base64.StdEncoding.EncodeToString(hash),
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))
515 type hashedHost struct {
520 const sha1HashType = "1"
522 func newHashedHost(encoded string) (*hashedHost, error) {
523 typ, salt, hash, err := decodeHash(encoded)
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)
535 return &hashedHost{salt: salt, hash: hash}, nil
538 func (h *hashedHost) match(a addr) bool {
539 return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)