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 }