github.com/devops-filetransfer/sshego@v7.0.4+incompatible/_vendor/golang.org/x/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 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 "golang.org/x/crypto/ssh" 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 // IsAuthority can be used as a callback in ssh.CertChecker 148 func (db *hostKeyDB) IsAuthority(remote ssh.PublicKey) bool { 149 for _, l := range db.lines { 150 // TODO(hanwen): should we check the hostname against host pattern? 151 if l.cert && keyEq(l.knownKey.Key, remote) { 152 return true 153 } 154 } 155 return false 156 } 157 158 // IsRevoked can be used as a callback in ssh.CertChecker 159 func (db *hostKeyDB) IsRevoked(key *ssh.Certificate) bool { 160 _, ok := db.revoked[string(key.Marshal())] 161 return ok 162 } 163 164 const markerCert = "@cert-authority" 165 const markerRevoked = "@revoked" 166 167 func nextWord(line []byte) (string, []byte) { 168 i := bytes.IndexAny(line, "\t ") 169 if i == -1 { 170 return string(line), nil 171 } 172 173 return string(line[:i]), bytes.TrimSpace(line[i:]) 174 } 175 176 func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) { 177 if w, next := nextWord(line); w == markerCert || w == markerRevoked { 178 marker = w 179 line = next 180 } 181 182 host, line = nextWord(line) 183 if len(line) == 0 { 184 return "", "", nil, errors.New("knownhosts: missing host pattern") 185 } 186 187 // ignore the keytype as it's in the key blob anyway. 188 _, line = nextWord(line) 189 if len(line) == 0 { 190 return "", "", nil, errors.New("knownhosts: missing key type pattern") 191 } 192 193 keyBlob, _ := nextWord(line) 194 195 keyBytes, err := base64.StdEncoding.DecodeString(keyBlob) 196 if err != nil { 197 return "", "", nil, err 198 } 199 key, err = ssh.ParsePublicKey(keyBytes) 200 if err != nil { 201 return "", "", nil, err 202 } 203 204 return marker, host, key, nil 205 } 206 207 func (db *hostKeyDB) parseLine(line []byte, filename string, linenum int) error { 208 marker, pattern, key, err := parseLine(line) 209 if err != nil { 210 return err 211 } 212 213 if marker == markerRevoked { 214 db.revoked[string(key.Marshal())] = &KnownKey{ 215 Key: key, 216 Filename: filename, 217 Line: linenum, 218 } 219 220 return nil 221 } 222 223 entry := keyDBLine{ 224 cert: marker == markerCert, 225 knownKey: KnownKey{ 226 Filename: filename, 227 Line: linenum, 228 Key: key, 229 }, 230 } 231 232 if pattern[0] == '|' { 233 entry.matcher, err = newHashedHost(pattern) 234 } else { 235 entry.matcher, err = newHostnameMatcher(pattern) 236 } 237 238 if err != nil { 239 return err 240 } 241 242 db.lines = append(db.lines, entry) 243 return nil 244 } 245 246 func newHostnameMatcher(pattern string) (matcher, error) { 247 var hps hostPatterns 248 for _, p := range strings.Split(pattern, ",") { 249 if len(p) == 0 { 250 continue 251 } 252 253 var a addr 254 var negate bool 255 if p[0] == '!' { 256 negate = true 257 p = p[1:] 258 } 259 260 if len(p) == 0 { 261 return nil, errors.New("knownhosts: negation without following hostname") 262 } 263 264 var err error 265 if p[0] == '[' { 266 a.host, a.port, err = net.SplitHostPort(p) 267 if err != nil { 268 return nil, err 269 } 270 } else { 271 a.host, a.port, err = net.SplitHostPort(p) 272 if err != nil { 273 a.host = p 274 a.port = "22" 275 } 276 } 277 hps = append(hps, hostPattern{ 278 negate: negate, 279 addr: a, 280 }) 281 } 282 return hps, nil 283 } 284 285 // KnownKey represents a key declared in a known_hosts file. 286 type KnownKey struct { 287 Key ssh.PublicKey 288 Filename string 289 Line int 290 } 291 292 func (k *KnownKey) String() string { 293 return fmt.Sprintf("%s:%d: %s", k.Filename, k.Line, serialize(k.Key)) 294 } 295 296 // KeyError is returned if we did not find the key in the host key 297 // database, or there was a mismatch. Typically, in batch 298 // applications, this should be interpreted as failure. Interactive 299 // applications can offer an interactive prompt to the user. 300 type KeyError struct { 301 // Want holds the accepted host keys. For each key algorithm, 302 // there can be one hostkey. If Want is empty, the host is 303 // unknown. If Want is non-empty, there was a mismatch, which 304 // can signify a MITM attack. 305 Want []KnownKey 306 } 307 308 func (u *KeyError) Error() string { 309 if len(u.Want) == 0 { 310 return "knownhosts: key is unknown" 311 } 312 return "knownhosts: key mismatch" 313 } 314 315 // RevokedError is returned if we found a key that was revoked. 316 type RevokedError struct { 317 Revoked KnownKey 318 } 319 320 func (r *RevokedError) Error() string { 321 return "knownhosts: key is revoked" 322 } 323 324 // check checks a key against the host database. This should not be 325 // used for verifying certificates. 326 func (db *hostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error { 327 if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil { 328 return &RevokedError{Revoked: *revoked} 329 } 330 331 host, port, err := net.SplitHostPort(remote.String()) 332 if err != nil { 333 return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err) 334 } 335 336 addrs := []addr{ 337 {host, port}, 338 } 339 340 if address != "" { 341 host, port, err := net.SplitHostPort(address) 342 if err != nil { 343 return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err) 344 } 345 346 addrs = append(addrs, addr{host, port}) 347 } 348 349 return db.checkAddrs(addrs, remoteKey) 350 } 351 352 // checkAddrs checks if we can find the given public key for any of 353 // the given addresses. If we only find an entry for the IP address, 354 // or only the hostname, then this still succeeds. 355 func (db *hostKeyDB) checkAddrs(addrs []addr, remoteKey ssh.PublicKey) error { 356 // TODO(hanwen): are these the right semantics? What if there 357 // is just a key for the IP address, but not for the 358 // hostname? 359 360 // Algorithm => key. 361 knownKeys := map[string]KnownKey{} 362 for _, l := range db.lines { 363 if l.match(addrs) { 364 typ := l.knownKey.Key.Type() 365 if _, ok := knownKeys[typ]; !ok { 366 knownKeys[typ] = l.knownKey 367 } 368 } 369 } 370 371 keyErr := &KeyError{} 372 for _, v := range knownKeys { 373 keyErr.Want = append(keyErr.Want, v) 374 } 375 376 // Unknown remote host. 377 if len(knownKeys) == 0 { 378 return keyErr 379 } 380 381 // If the remote host starts using a different, unknown key type, we 382 // also interpret that as a mismatch. 383 if known, ok := knownKeys[remoteKey.Type()]; !ok || !keyEq(known.Key, remoteKey) { 384 return keyErr 385 } 386 387 return nil 388 } 389 390 // The Read function parses file contents. 391 func (db *hostKeyDB) Read(r io.Reader, filename string) error { 392 scanner := bufio.NewScanner(r) 393 394 lineNum := 0 395 for scanner.Scan() { 396 lineNum++ 397 line := scanner.Bytes() 398 line = bytes.TrimSpace(line) 399 if len(line) == 0 || line[0] == '#' { 400 continue 401 } 402 403 if err := db.parseLine(line, filename, lineNum); err != nil { 404 return fmt.Errorf("knownhosts: %s:%d: %v", filename, lineNum, err) 405 } 406 } 407 return scanner.Err() 408 } 409 410 // New creates a host key callback from the given OpenSSH host key 411 // files. The returned callback is for use in 412 // ssh.ClientConfig.HostKeyCallback. Hostnames are ignored for 413 // certificates, ie. any certificate authority is assumed to be valid 414 // for all remote hosts. Hashed hostnames are not supported. 415 func New(files ...string) (ssh.HostKeyCallback, error) { 416 db := newHostKeyDB() 417 for _, fn := range files { 418 f, err := os.Open(fn) 419 if err != nil { 420 return nil, err 421 } 422 defer f.Close() 423 if err := db.Read(f, fn); err != nil { 424 return nil, err 425 } 426 } 427 428 // TODO(hanwen): properly supporting certificates requires an 429 // API change in the SSH library: IsAuthority should provide 430 // the address too? 431 432 var certChecker ssh.CertChecker 433 certChecker.IsAuthority = db.IsAuthority 434 certChecker.IsRevoked = db.IsRevoked 435 certChecker.HostKeyFallback = db.check 436 437 return certChecker.CheckHostKey, nil 438 } 439 440 // Normalize normalizes an address into the form used in known_hosts 441 func Normalize(address string) string { 442 host, port, err := net.SplitHostPort(address) 443 if err != nil { 444 host = address 445 port = "22" 446 } 447 entry := host 448 if port != "22" { 449 entry = "[" + entry + "]:" + port 450 } else if strings.Contains(host, ":") && !strings.HasPrefix(host, "[") { 451 entry = "[" + entry + "]" 452 } 453 return entry 454 } 455 456 // Line returns a line to add append to the known_hosts files. 457 func Line(addresses []string, key ssh.PublicKey) string { 458 var trimmed []string 459 for _, a := range addresses { 460 trimmed = append(trimmed, Normalize(a)) 461 } 462 463 return strings.Join(trimmed, ",") + " " + serialize(key) 464 } 465 466 // HashHostname hashes the given hostname. The hostname is not 467 // normalized before hashing. 468 func HashHostname(hostname string) string { 469 // TODO(hanwen): check if we can safely normalize this always. 470 salt := make([]byte, sha1.Size) 471 472 _, err := rand.Read(salt) 473 if err != nil { 474 panic(fmt.Sprintf("crypto/rand failure %v", err)) 475 } 476 477 hash := hashHost(hostname, salt) 478 return encodeHash(sha1HashType, salt, hash) 479 } 480 481 func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) { 482 if len(encoded) == 0 || encoded[0] != '|' { 483 err = errors.New("knownhosts: hashed host must start with '|'") 484 return 485 } 486 components := strings.Split(encoded, "|") 487 if len(components) != 4 { 488 err = fmt.Errorf("knownhosts: got %d components, want 3", len(components)) 489 return 490 } 491 492 hashType = components[1] 493 if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil { 494 return 495 } 496 if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil { 497 return 498 } 499 return 500 } 501 502 func encodeHash(typ string, salt []byte, hash []byte) string { 503 return strings.Join([]string{"", 504 typ, 505 base64.StdEncoding.EncodeToString(salt), 506 base64.StdEncoding.EncodeToString(hash), 507 }, "|") 508 } 509 510 // See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120 511 func hashHost(hostname string, salt []byte) []byte { 512 mac := hmac.New(sha1.New, salt) 513 mac.Write([]byte(hostname)) 514 return mac.Sum(nil) 515 } 516 517 type hashedHost struct { 518 salt []byte 519 hash []byte 520 } 521 522 const sha1HashType = "1" 523 524 func newHashedHost(encoded string) (*hashedHost, error) { 525 typ, salt, hash, err := decodeHash(encoded) 526 if err != nil { 527 return nil, err 528 } 529 530 // The type field seems for future algorithm agility, but it's 531 // actually hardcoded in openssh currently, see 532 // https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120 533 if typ != sha1HashType { 534 return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ) 535 } 536 537 return &hashedHost{salt: salt, hash: hash}, nil 538 } 539 540 func (h *hashedHost) match(addrs []addr) bool { 541 for _, a := range addrs { 542 if bytes.Equal(hashHost(Normalize(a.String()), h.salt), h.hash) { 543 return true 544 } 545 } 546 return false 547 }