Skip to content

Commit 302a543

Browse files
ehusspietroalbini
authored andcommitted
Add some known_hosts tests.
This also fixes a bug with the host matching when there are comma-separated hosts.
1 parent 026bda3 commit 302a543

File tree

1 file changed

+148
-12
lines changed

1 file changed

+148
-12
lines changed

src/cargo/sources/git/known_hosts.rs

Lines changed: 148 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
//! this file.
2222
2323
use crate::util::config::{Definition, Value};
24-
use git2::cert::Cert;
24+
use git2::cert::{Cert, SshHostKeyType};
2525
use git2::CertificateCheckStatus;
2626
use std::collections::HashSet;
2727
use std::fmt::Write;
@@ -49,15 +49,15 @@ enum KnownHostError {
4949
/// The host key was not found.
5050
HostKeyNotFound {
5151
hostname: String,
52-
key_type: git2::cert::SshHostKeyType,
52+
key_type: SshHostKeyType,
5353
remote_host_key: String,
5454
remote_fingerprint: String,
5555
other_hosts: Vec<KnownHost>,
5656
},
5757
/// The host key was found, but does not match the remote's key.
5858
HostKeyHasChanged {
5959
hostname: String,
60-
key_type: git2::cert::SshHostKeyType,
60+
key_type: SshHostKeyType,
6161
old_known_host: KnownHost,
6262
remote_host_key: String,
6363
remote_fingerprint: String,
@@ -238,11 +238,6 @@ fn check_ssh_known_hosts(
238238
return Err(anyhow::format_err!("remote host key is not available").into());
239239
};
240240
let remote_key_type = cert_host_key.hostkey_type().unwrap();
241-
// `changed_key` keeps track of any entries where the key has changed.
242-
let mut changed_key = None;
243-
// `other_hosts` keeps track of any entries that have an identical key,
244-
// but a different hostname.
245-
let mut other_hosts = Vec::new();
246241

247242
// Collect all the known host entries from disk.
248243
let mut known_hosts = Vec::new();
@@ -293,6 +288,21 @@ fn check_ssh_known_hosts(
293288
});
294289
}
295290
}
291+
check_ssh_known_hosts_loaded(&known_hosts, host, remote_key_type, remote_host_key)
292+
}
293+
294+
/// Checks a host key against a loaded set of known hosts.
295+
fn check_ssh_known_hosts_loaded(
296+
known_hosts: &[KnownHost],
297+
host: &str,
298+
remote_key_type: SshHostKeyType,
299+
remote_host_key: &[u8],
300+
) -> Result<(), KnownHostError> {
301+
// `changed_key` keeps track of any entries where the key has changed.
302+
let mut changed_key = None;
303+
// `other_hosts` keeps track of any entries that have an identical key,
304+
// but a different hostname.
305+
let mut other_hosts = Vec::new();
296306

297307
for known_host in known_hosts {
298308
// The key type from libgit2 needs to match the key type from the host file.
@@ -301,7 +311,6 @@ fn check_ssh_known_hosts(
301311
}
302312
let key_matches = known_host.key == remote_host_key;
303313
if !known_host.host_matches(host) {
304-
// `name` can be None for hashed hostnames (which libgit2 does not expose).
305314
if key_matches {
306315
other_hosts.push(known_host.clone());
307316
}
@@ -434,7 +443,7 @@ impl KnownHost {
434443
return false;
435444
}
436445
} else {
437-
match_found = pattern == host;
446+
match_found |= pattern == host;
438447
}
439448
}
440449
match_found
@@ -444,6 +453,10 @@ impl KnownHost {
444453
/// Loads an OpenSSH known_hosts file.
445454
fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
446455
let contents = cargo_util::paths::read(path)?;
456+
Ok(load_hostfile_contents(path, &contents))
457+
}
458+
459+
fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> {
447460
let entries = contents
448461
.lines()
449462
.enumerate()
@@ -455,13 +468,13 @@ fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
455468
parse_known_hosts_line(line, location)
456469
})
457470
.collect();
458-
Ok(entries)
471+
entries
459472
}
460473

461474
fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> {
462475
let line = line.trim();
463476
// FIXME: @revoked and @cert-authority is currently not supported.
464-
if line.is_empty() || line.starts_with('#') || line.starts_with('@') {
477+
if line.is_empty() || line.starts_with(['#', '@', '|']) {
465478
return None;
466479
}
467480
let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty());
@@ -476,3 +489,126 @@ fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<Kno
476489
key,
477490
})
478491
}
492+
493+
#[cfg(test)]
494+
mod tests {
495+
use super::*;
496+
497+
static COMMON_CONTENTS: &str = r#"
498+
# Comments allowed at start of line
499+
500+
example.com,rust-lang.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5MzWIpZwpkpDjyCNiTIEVFhSA9OUUQvjFo7CgZBGCAj/cqeUIgiLsgtfmtBsfWIkAECQpM7ePP7NLZFGJcHvoyg5jXJiIX5s0eKo9IlcuTLLrMkW5MkHXE7bNklVbW1WdCfF2+y7Ao25B4L8FFRokMh0yp/H6+8xZ7PdVwL3FRPEg8ftZ5R0kuups6xiMHPRX+f/07vfJzA47YDPmXfhkn+JK8kL0JYw8iy8BtNBfRQL99d9iXJzWXnNce5NHMuKD5rOonD3aQHLDlwK+KhrFRrdaxQEM8ZWxNti0ux8yT4Dl5jJY0CrIu3Xl6+qroVgTqJGNkTbhs5DGWdFh6BLPTTH15rN4buisg7uMyLyHqx06ckborqD33gWu+Jig7O+PV6KJmL5mp1O1HXvZqkpBdTiT6GiDKG3oECCIXkUk0BSU9VG9VQcrMxxvgiHlyoXUAfYQoXv/lnxkTnm+Sr36kutsVOs7n5B43ZKAeuaxyQ11huJZpxamc0RA1HM641s= eric@host
501+
Example.net ssh-dss AAAAB3NzaC1kc3MAAACBAK2Ek3jVxisXmz5UcZ7W65BAj/nDJCCVvSe0Aytndn4PH6k7sVesut5OoY6PdksZ9tEfuFjjS9HR5SJb8j1GW0GxtaSHHbf+rNc36PeU75bffzyIWwpA8uZFONt5swUAXJXcsHOoapNbUFuhHsRhB2hXxz9QGNiiwIwRJeSHixKRAAAAFQChKfxO1z9H2/757697xP5nJ/Z5dwAAAIEAoc+HIWas+4WowtB/KtAp6XE0B9oHI+55wKtdcGwwb7zHKK9scWNXwxIcMhSvyB3Oe2I7dQQlvyIWxsdZlzOkX0wdsTHjIAnBAP68MyvMv4kq3+I5GAVcFsqoLZfZvh0dlcgUq1/YNYZwKlt89tnzk8Fp4KLWmuw8Bd8IShYVa78AAACAL3qd8kNTY7CthgsQ8iWdjbkGSF/1KCeFyt8UjurInp9wvPDjqagwakbyLOzN7y3/ItTPCaGuX+RjFP0zZTf8i9bsAVyjFJiJ7vzRXcWytuFWANrpzLTn1qzPfh63iK92Aw8AVBYvEA/4bxo+XReAvhNBB/m78G6OedTeu6ZoTsI= eric@host
502+
[example.net]:2222 ssh-dss AAAAB3NzaC1kc3MAAACBAJJN5kLZEpOJpXWyMT4KwYvLAj+b9ErNtglxOi86C6Kw7oZeYdDMCfD3lc3PJyX64udQcWGfO4abSESMiYdY43yFAZH279QGH5Q/B5CklVvTqYpfAUR+1r9TQxy3OVQHk7FB2wOi4xNQ3myO0vaYlBOB9il+P223aERbXx4JTWdvAAAAFQCTHWTcXxLK5Z6ZVPmfdSDyHzkF2wAAAIEAhp41/mTnM0Y0EWSyCXuETMW1QSpKGF8sqoZKp6wdzyhLXu0i32gLdXj4p24em/jObYh93hr+MwgxqWq+FHgD+D80Qg5f6vj4yEl4Uu5hqtTpCBFWUQoyEckbUkPf8uZ4/XzAne+tUSjZm09xATCmK9U2IGqZE+D+90eBkf1Svc8AAACAeKhi4EtfwenFYqKz60ZoEEhIsE1yI2jH73akHnfHpcW84w+fk3YlwjcfDfyYso+D0jZBdJeK5qIdkbUWhAX8wDjJVO0WL6r/YPr4yu/CgEyW1H59tAbujGJ4NR0JDqioulzYqNHnxpiw1RJukZnPBfSFKzRElvPOCq/NkQM/Mwk= eric@host
503+
nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host
504+
nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host
505+
nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host
506+
# Revoked not yet supported.
507+
@revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
508+
example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
509+
192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
510+
# Hash not yet supported.
511+
|1|7CMSYgzdwruFLRhwowMtKx0maIE=|Tlff1GFqc3Ao+fUWxMEVG8mJiyk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
512+
# Negation isn't terribly useful without globs.
513+
neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host
514+
"#;
515+
516+
#[test]
517+
fn known_hosts_parse() {
518+
let kh_path = Path::new("/home/abc/.known_hosts");
519+
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
520+
assert_eq!(khs.len(), 9);
521+
match &khs[0].location {
522+
KnownHostLocation::File { path, lineno } => {
523+
assert_eq!(path, kh_path);
524+
assert_eq!(*lineno, 4);
525+
}
526+
_ => panic!("unexpected"),
527+
}
528+
assert_eq!(khs[0].patterns, "example.com,rust-lang.org");
529+
assert_eq!(khs[0].key_type, "ssh-rsa");
530+
assert_eq!(khs[0].key.len(), 407);
531+
assert_eq!(&khs[0].key[..30], b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x03\x01\x00\x01\x00\x00\x01\x81\x00\xb935\x88\xa5\x9c)");
532+
match &khs[1].location {
533+
KnownHostLocation::File { path, lineno } => {
534+
assert_eq!(path, kh_path);
535+
assert_eq!(*lineno, 5);
536+
}
537+
_ => panic!("unexpected"),
538+
}
539+
assert_eq!(khs[2].patterns, "[example.net]:2222");
540+
assert_eq!(khs[3].patterns, "nistp256.example.org");
541+
assert_eq!(khs[7].patterns, "192.168.42.12");
542+
}
543+
544+
#[test]
545+
fn host_matches() {
546+
let kh_path = Path::new("/home/abc/.known_hosts");
547+
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
548+
assert!(khs[0].host_matches("example.com"));
549+
assert!(khs[0].host_matches("rust-lang.org"));
550+
assert!(khs[0].host_matches("EXAMPLE.COM"));
551+
assert!(khs[1].host_matches("example.net"));
552+
assert!(!khs[0].host_matches("example.net"));
553+
assert!(khs[2].host_matches("[example.net]:2222"));
554+
assert!(!khs[2].host_matches("example.net"));
555+
assert!(!khs[8].host_matches("neg.example.com"));
556+
}
557+
558+
#[test]
559+
fn check_match() {
560+
let kh_path = Path::new("/home/abc/.known_hosts");
561+
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
562+
563+
assert!(check_ssh_known_hosts_loaded(
564+
&khs,
565+
"example.com",
566+
SshHostKeyType::Rsa,
567+
&khs[0].key
568+
)
569+
.is_ok());
570+
571+
match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Dss, &khs[0].key) {
572+
Err(KnownHostError::HostKeyNotFound {
573+
hostname,
574+
remote_fingerprint,
575+
other_hosts,
576+
..
577+
}) => {
578+
assert_eq!(
579+
remote_fingerprint,
580+
"yn+pONDn0EcgdOCVptgB4RZd/wqmsVKrPnQMLtrvhw8"
581+
);
582+
assert_eq!(hostname, "example.com");
583+
assert_eq!(other_hosts.len(), 0);
584+
}
585+
_ => panic!("unexpected"),
586+
}
587+
588+
match check_ssh_known_hosts_loaded(
589+
&khs,
590+
"foo.example.com",
591+
SshHostKeyType::Rsa,
592+
&khs[0].key,
593+
) {
594+
Err(KnownHostError::HostKeyNotFound { other_hosts, .. }) => {
595+
assert_eq!(other_hosts.len(), 1);
596+
assert_eq!(other_hosts[0].patterns, "example.com,rust-lang.org");
597+
}
598+
_ => panic!("unexpected"),
599+
}
600+
601+
let mut modified_key = khs[0].key.clone();
602+
modified_key[0] = 1;
603+
match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Rsa, &modified_key)
604+
{
605+
Err(KnownHostError::HostKeyHasChanged { old_known_host, .. }) => {
606+
assert!(matches!(
607+
old_known_host.location,
608+
KnownHostLocation::File { lineno: 4, .. }
609+
));
610+
}
611+
_ => panic!("unexpected"),
612+
}
613+
}
614+
}

0 commit comments

Comments
 (0)