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 }