Skip to content

Commit 67ae2dc

Browse files
ehusspietroalbini
authored andcommitted
ssh known_hosts: support hashed hostnames
1 parent 018403c commit 67ae2dc

File tree

2 files changed

+26
-9
lines changed

2 files changed

+26
-9
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ git2 = "0.16.0"
3232
git2-curl = "0.17.0"
3333
glob = "0.3.0"
3434
hex = "0.4"
35+
hmac = "0.12.1"
3536
home = "0.5"
3637
http-auth = { version = "0.1.6", default-features = false }
3738
humantime = "2.0.0"
@@ -56,6 +57,7 @@ serde = { version = "1.0.123", features = ["derive"] }
5657
serde_ignored = "0.1.0"
5758
serde_json = { version = "1.0.30", features = ["raw_value"] }
5859
serde-value = "0.7.0"
60+
sha1 = "0.10.5"
5961
shell-escape = "0.1.4"
6062
strip-ansi-escapes = "0.1.0"
6163
tar = { version = "0.4.38", default-features = false }

src/cargo/sources/git/known_hosts.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
//! - `VerifyHostKeyDNS` — Uses SSHFP DNS records to fetch a host key.
1717
//!
1818
//! There's also a number of things that aren't supported but could be easily
19-
//! added (it just adds a little complexity). For example, hashed hostnames,
20-
//! hostname patterns, and revoked markers. See "FIXME" comments littered in
21-
//! this file.
19+
//! added (it just adds a little complexity). For example, hostname patterns,
20+
//! and revoked markers. See "FIXME" comments littered in this file.
2221
2322
use crate::util::config::{Definition, Value};
2423
use git2::cert::{Cert, SshHostKeyType};
2524
use git2::CertificateCheckStatus;
25+
use hmac::Mac;
2626
use std::collections::HashSet;
2727
use std::fmt::Write;
2828
use std::path::{Path, PathBuf};
@@ -419,6 +419,8 @@ fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
419419
)
420420
}
421421

422+
const HASH_HOSTNAME_PREFIX: &str = "|1|";
423+
422424
/// A single known host entry.
423425
#[derive(Clone)]
424426
struct KnownHost {
@@ -434,7 +436,9 @@ impl KnownHost {
434436
fn host_matches(&self, host: &str) -> bool {
435437
let mut match_found = false;
436438
let host = host.to_lowercase();
437-
// FIXME: support hashed hostnames
439+
if let Some(hashed) = self.patterns.strip_prefix(HASH_HOSTNAME_PREFIX) {
440+
return hashed_hostname_matches(&host, hashed);
441+
}
438442
for pattern in self.patterns.split(',') {
439443
let pattern = pattern.to_lowercase();
440444
// FIXME: support * and ? wildcards
@@ -450,6 +454,16 @@ impl KnownHost {
450454
}
451455
}
452456

457+
fn hashed_hostname_matches(host: &str, hashed: &str) -> bool {
458+
let Some((b64_salt, b64_host)) = hashed.split_once('|') else { return false; };
459+
let Ok(salt) = base64::decode(b64_salt) else { return false; };
460+
let Ok(hashed_host) = base64::decode(b64_host) else { return false; };
461+
let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt) else { return false; };
462+
mac.update(host.as_bytes());
463+
let result = mac.finalize().into_bytes();
464+
hashed_host == &result[..]
465+
}
466+
453467
/// Loads an OpenSSH known_hosts file.
454468
fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
455469
let contents = cargo_util::paths::read(path)?;
@@ -474,7 +488,7 @@ fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> {
474488
fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> {
475489
let line = line.trim();
476490
// FIXME: @revoked and @cert-authority is currently not supported.
477-
if line.is_empty() || line.starts_with(['#', '@', '|']) {
491+
if line.is_empty() || line.starts_with(['#', '@']) {
478492
return None;
479493
}
480494
let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty());
@@ -506,8 +520,7 @@ mod tests {
506520
@revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
507521
example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
508522
192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
509-
# Hash not yet supported.
510-
|1|7CMSYgzdwruFLRhwowMtKx0maIE=|Tlff1GFqc3Ao+fUWxMEVG8mJiyk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
523+
|1|QxzZoTXIWLhUsuHAXjuDMIV3FjQ=|M6NCOIkjiWdCWqkh5+Q+/uFLGjs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
511524
# Negation isn't terribly useful without globs.
512525
neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host
513526
"#;
@@ -516,7 +529,7 @@ mod tests {
516529
fn known_hosts_parse() {
517530
let kh_path = Path::new("/home/abc/.known_hosts");
518531
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
519-
assert_eq!(khs.len(), 9);
532+
assert_eq!(khs.len(), 10);
520533
match &khs[0].location {
521534
KnownHostLocation::File { path, lineno } => {
522535
assert_eq!(path, kh_path);
@@ -551,7 +564,9 @@ mod tests {
551564
assert!(!khs[0].host_matches("example.net"));
552565
assert!(khs[2].host_matches("[example.net]:2222"));
553566
assert!(!khs[2].host_matches("example.net"));
554-
assert!(!khs[8].host_matches("neg.example.com"));
567+
assert!(khs[8].host_matches("hashed.example.com"));
568+
assert!(!khs[8].host_matches("example.com"));
569+
assert!(!khs[9].host_matches("neg.example.com"));
555570
}
556571

557572
#[test]

0 commit comments

Comments
 (0)