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. Supports
425// IPv4, hostnames, bracketed IPv6. Any other non-standard formats are returned
426// with minimal transformation.
427func Normalize(address string) string {
428 const defaultSSHPort = "22"
429
430 host, port, err := net.SplitHostPort(address)
431 if err != nil {
432 host = address
433 port = defaultSSHPort
434 }
435
436 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
437 host = host[1 : len(host)-1]
438 }
439
440 if port == defaultSSHPort {
441 return host
442 }
443 return "[" + host + "]:" + port
444}
445
446// Line returns a line to add append to the known_hosts files.
447func Line(addresses []string, key ssh.PublicKey) string {
448 var trimmed []string
449 for _, a := range addresses {
450 trimmed = append(trimmed, Normalize(a))
451 }
452
453 return strings.Join(trimmed, ",") + " " + serialize(key)
454}
455
456// HashHostname hashes the given hostname. The hostname is not
457// normalized before hashing.
458func HashHostname(hostname string) string {
459 // TODO(hanwen): check if we can safely normalize this always.
460 salt := make([]byte, sha1.Size)
461
462 _, err := rand.Read(salt)
463 if err != nil {
464 panic(fmt.Sprintf("crypto/rand failure %v", err))
465 }
466
467 hash := hashHost(hostname, salt)
468 return encodeHash(sha1HashType, salt, hash)
469}
470
471func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
472 if len(encoded) == 0 || encoded[0] != '|' {
473 err = errors.New("knownhosts: hashed host must start with '|'")
474 return
475 }
476 components := strings.Split(encoded, "|")
477 if len(components) != 4 {
478 err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
479 return
480 }
481
482 hashType = components[1]
483 if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
484 return
485 }
486 if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
487 return
488 }
489 return
490}
491
492func encodeHash(typ string, salt []byte, hash []byte) string {
493 return strings.Join([]string{"",
494 typ,
495 base64.StdEncoding.EncodeToString(salt),
496 base64.StdEncoding.EncodeToString(hash),
497 }, "|")
498}
499
500// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
501func hashHost(hostname string, salt []byte) []byte {
502 mac := hmac.New(sha1.New, salt)
503 mac.Write([]byte(hostname))
504 return mac.Sum(nil)
505}
506
507type hashedHost struct {
508 salt []byte
509 hash []byte
510}
511
512const sha1HashType = "1"
513
514func newHashedHost(encoded string) (*hashedHost, error) {
515 typ, salt, hash, err := decodeHash(encoded)
516 if err != nil {
517 return nil, err
518 }
519
520 // The type field seems for future algorithm agility, but it's
521 // actually hardcoded in openssh currently, see
522 // https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
523 if typ != sha1HashType {
524 return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
525 }
526
527 return &hashedHost{salt: salt, hash: hash}, nil
528}
529
530func (h *hashedHost) match(a addr) bool {
531 return bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash)
532}