1use crate::CargoResult;
26use crate::util::context::{Definition, GlobalContext, Value};
27use crate::util::restricted_names::is_glob_pattern;
28use base64::Engine as _;
29use base64::engine::general_purpose::STANDARD;
30use base64::engine::general_purpose::STANDARD_NO_PAD;
31use git2::CertificateCheckStatus;
32use git2::cert::{Cert, SshHostKeyType};
33use hmac::Mac;
34use std::collections::HashSet;
35use std::fmt::{Display, Write};
36use std::path::{Path, PathBuf};
37
38static BUNDLED_KEYS: &[(&str, &str, &str)] = &[
49 (
50 "github.com",
51 "ssh-ed25519",
52 "AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl",
53 ),
54 (
55 "github.com",
56 "ecdsa-sha2-nistp256",
57 "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=",
58 ),
59 (
60 "github.com",
61 "ssh-rsa",
62 "AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=",
63 ),
64];
65
66static BUNDLED_REVOCATIONS: &[(&str, &str, &str)] = &[
76 (
78 "github.com",
79 "ssh-rsa",
80 "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==",
81 ),
82];
83
84enum KnownHostError {
85 CheckError(anyhow::Error),
87 HostKeyNotFound {
89 hostname: String,
90 key_type: SshHostKeyType,
91 remote_host_key: String,
92 remote_fingerprint: String,
93 other_hosts: Vec<KnownHost>,
94 },
95 HostKeyHasChanged {
97 hostname: String,
98 key_type: SshHostKeyType,
99 old_known_host: KnownHost,
100 remote_host_key: String,
101 remote_fingerprint: String,
102 },
103 HostKeyRevoked {
105 hostname: String,
106 key_type: SshHostKeyType,
107 remote_host_key: String,
108 location: KnownHostLocation,
109 },
110 HostHasOnlyCertAuthority {
113 hostname: String,
114 location: KnownHostLocation,
115 },
116}
117
118impl From<anyhow::Error> for KnownHostError {
119 fn from(err: anyhow::Error) -> KnownHostError {
120 KnownHostError::CheckError(err)
121 }
122}
123
124#[derive(Clone)]
126enum KnownHostLocation {
127 File { path: PathBuf, lineno: u32 },
129 Config { definition: Definition },
131 Bundled,
133}
134
135impl Display for KnownHostLocation {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 let loc = match self {
138 KnownHostLocation::File { path, lineno } => {
139 format!("{} line {lineno}", path.display())
140 }
141 KnownHostLocation::Config { definition } => {
142 format!("config value from {definition}")
143 }
144 KnownHostLocation::Bundled => format!("bundled with cargo"),
145 };
146 f.write_str(&loc)
147 }
148}
149
150pub fn certificate_check(
152 gctx: &GlobalContext,
153 cert: &Cert<'_>,
154 host: &str,
155 port: Option<u16>,
156 config_known_hosts: Option<&Vec<Value<String>>>,
157 diagnostic_home_config: &str,
158) -> CargoResult<CertificateCheckStatus> {
159 let Some(host_key) = cert.as_hostkey() else {
160 return Ok(CertificateCheckStatus::CertificatePassthrough);
163 };
164 let host_maybe_port = match port {
167 Some(port) if port != 22 => format!("[{host}]:{port}"),
168 _ => host.to_string(),
169 };
170 match check_ssh_known_hosts(gctx, host_key, &host_maybe_port, config_known_hosts) {
172 Ok(()) => {
173 return Ok(CertificateCheckStatus::CertificateOk);
174 }
175 Err(KnownHostError::CheckError(e)) => {
176 anyhow::bail!("error: failed to validate host key:\n{:#}", e)
177 }
178 Err(KnownHostError::HostKeyNotFound {
179 hostname,
180 key_type,
181 remote_host_key,
182 remote_fingerprint,
183 other_hosts,
184 }) => {
185 if port.is_some()
187 && !matches!(port, Some(22))
188 && check_ssh_known_hosts(gctx, host_key, host, config_known_hosts).is_ok()
189 {
190 return Ok(CertificateCheckStatus::CertificateOk);
191 }
192 let key_type_short_name = key_type.short_name();
193 let key_type_name = key_type.name();
194 let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
195 let other_hosts_message = if other_hosts.is_empty() {
196 String::new()
197 } else {
198 let mut msg = String::from(
199 "Note: This host key was found, \
200 but is associated with a different host:\n",
201 );
202 for known_host in other_hosts {
203 write!(
204 msg,
205 " {loc}: {patterns}\n",
206 loc = known_host.location,
207 patterns = known_host.patterns
208 )
209 .unwrap();
210 }
211 msg
212 };
213 anyhow::bail!(
214 "error: unknown SSH host key\n\
215 The SSH host key for `{hostname}` is not known and cannot be validated.\n\
216 \n\
217 To resolve this issue, add the host key to {known_hosts_location}\n\
218 \n\
219 The key to add is:\n\
220 \n\
221 {hostname} {key_type_name} {remote_host_key}\n\
222 \n\
223 The {key_type_short_name} key fingerprint is: SHA256:{remote_fingerprint}\n\
224 This fingerprint should be validated with the server administrator that it is correct.\n\
225 {other_hosts_message}\n\
226 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
227 for more information.\n\
228 "
229 )
230 }
231 Err(KnownHostError::HostKeyHasChanged {
232 hostname,
233 key_type,
234 old_known_host,
235 remote_host_key,
236 remote_fingerprint,
237 }) => {
238 let key_type_short_name = key_type.short_name();
239 let key_type_name = key_type.name();
240 let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
241 let old_key_resolution = match old_known_host.location {
242 KnownHostLocation::File { path, lineno } => {
243 let old_key_location = path.display();
244 format!(
245 "removing the old {key_type_name} key for `{hostname}` \
246 located at {old_key_location} line {lineno}, \
247 and adding the new key to {known_hosts_location}",
248 )
249 }
250 KnownHostLocation::Config { definition } => {
251 format!(
252 "removing the old {key_type_name} key for `{hostname}` \
253 loaded from Cargo's config at {definition}, \
254 and adding the new key to {known_hosts_location}"
255 )
256 }
257 KnownHostLocation::Bundled => {
258 format!(
259 "adding the new key to {known_hosts_location}\n\
260 The current host key is bundled as part of Cargo."
261 )
262 }
263 };
264 anyhow::bail!(
265 "error: SSH host key has changed for `{hostname}`\n\
266 *********************************\n\
267 * WARNING: HOST KEY HAS CHANGED *\n\
268 *********************************\n\
269 This may be caused by a man-in-the-middle attack, or the \
270 server may have changed its host key.\n\
271 \n\
272 The {key_type_short_name} fingerprint for the key from the remote host is:\n\
273 SHA256:{remote_fingerprint}\n\
274 \n\
275 You are strongly encouraged to contact the server \
276 administrator for `{hostname}` to verify that this new key is \
277 correct.\n\
278 \n\
279 If you can verify that the server has a new key, you can \
280 resolve this error by {old_key_resolution}\n\
281 \n\
282 The key provided by the remote host is:\n\
283 \n\
284 {hostname} {key_type_name} {remote_host_key}\n\
285 \n\
286 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
287 for more information.\n\
288 "
289 )
290 }
291 Err(KnownHostError::HostKeyRevoked {
292 hostname,
293 key_type,
294 remote_host_key,
295 location,
296 }) => {
297 let key_type_short_name = key_type.short_name();
298 anyhow::bail!(
299 "error: Key has been revoked for `{hostname}`\n\
300 **************************************\n\
301 * WARNING: REVOKED HOST KEY DETECTED *\n\
302 **************************************\n\
303 This may indicate that the key provided by this host has been\n\
304 compromised and should not be accepted.
305 \n\
306 The host key {key_type_short_name} {remote_host_key} is revoked\n\
307 in {location} and has been rejected.\n\
308 "
309 )
310 }
311 Err(KnownHostError::HostHasOnlyCertAuthority { hostname, location }) => {
312 anyhow::bail!("error: Found a `@cert-authority` marker for `{hostname}`\n\
313 \n\
314 Cargo doesn't support certificate authorities for host key verification. It is\n\
315 recommended that the command line Git client is used instead. This can be achieved\n\
316 by setting `net.git-fetch-with-cli` to `true` in the Cargo config.\n\
317 \n
318 The `@cert-authority` line was found in {location}.\n\
319 \n\
320 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
321 for more information.\n\
322 ")
323 }
324 }
325}
326
327fn check_ssh_known_hosts(
329 gctx: &GlobalContext,
330 cert_host_key: &git2::cert::CertHostkey<'_>,
331 host: &str,
332 config_known_hosts: Option<&Vec<Value<String>>>,
333) -> Result<(), KnownHostError> {
334 let Some(remote_host_key) = cert_host_key.hostkey() else {
335 return Err(anyhow::format_err!("remote host key is not available").into());
336 };
337 let remote_key_type = cert_host_key.hostkey_type().unwrap();
338
339 let mut known_hosts = Vec::new();
341 for path in known_host_files(gctx) {
342 if !path.exists() {
343 continue;
344 }
345 let hosts = load_hostfile(&path)?;
346 known_hosts.extend(hosts);
347 }
348 if let Some(config_known_hosts) = config_known_hosts {
349 for line_value in config_known_hosts {
352 let location = KnownHostLocation::Config {
353 definition: line_value.definition.clone(),
354 };
355 match parse_known_hosts_line(&line_value.val, location) {
356 Some(known_host) => known_hosts.push(known_host),
357 None => tracing::warn!(
358 "failed to parse known host {} from {}",
359 line_value.val,
360 line_value.definition
361 ),
362 }
363 }
364 }
365 let configured_hosts: HashSet<_> = known_hosts
369 .iter()
370 .flat_map(|known_host| {
371 known_host
372 .patterns
373 .split(',')
374 .map(|pattern| pattern.to_lowercase())
375 })
376 .collect();
377 for (patterns, key_type, key) in BUNDLED_KEYS {
378 if !configured_hosts.contains(*patterns) {
379 let key = STANDARD.decode(key).unwrap();
380 known_hosts.push(KnownHost {
381 location: KnownHostLocation::Bundled,
382 patterns: patterns.to_string(),
383 key_type: key_type.to_string(),
384 key,
385 line_type: KnownHostLineType::Key,
386 });
387 }
388 }
389 for (patterns, key_type, key) in BUNDLED_REVOCATIONS {
390 let key = STANDARD.decode(key).unwrap();
391 known_hosts.push(KnownHost {
392 location: KnownHostLocation::Bundled,
393 patterns: patterns.to_string(),
394 key_type: key_type.to_string(),
395 key,
396 line_type: KnownHostLineType::Revoked,
397 });
398 }
399 check_ssh_known_hosts_loaded(&known_hosts, host, remote_key_type, remote_host_key)
400}
401
402fn check_ssh_known_hosts_loaded(
404 known_hosts: &[KnownHost],
405 host: &str,
406 remote_key_type: SshHostKeyType,
407 remote_host_key: &[u8],
408) -> Result<(), KnownHostError> {
409 let mut latent_errors: Vec<KnownHostError> = Vec::new();
412
413 let mut other_hosts = Vec::new();
416
417 let mut accepted_known_host_found = false;
421
422 let mut remote_fingerprint = cargo_util::Sha256::new();
426 remote_fingerprint.update(remote_host_key);
427 let remote_fingerprint = STANDARD_NO_PAD.encode(remote_fingerprint.finish());
428 let remote_host_key_encoded = STANDARD.encode(remote_host_key);
429
430 for known_host in known_hosts {
431 if known_host.key_type != remote_key_type.name() {
433 continue;
434 }
435 let key_matches = known_host.key == remote_host_key;
436 if !known_host.host_matches(host) {
437 if key_matches {
438 other_hosts.push(known_host.clone());
439 }
440 continue;
441 }
442 match known_host.line_type {
443 KnownHostLineType::Key => {
444 if key_matches {
445 accepted_known_host_found = true;
446 } else {
447 latent_errors.push(KnownHostError::HostKeyHasChanged {
452 hostname: host.to_string(),
453 key_type: remote_key_type,
454 old_known_host: known_host.clone(),
455 remote_host_key: remote_host_key_encoded.clone(),
456 remote_fingerprint: remote_fingerprint.clone(),
457 });
458 }
459 }
460 KnownHostLineType::Revoked => {
461 if key_matches {
462 return Err(KnownHostError::HostKeyRevoked {
463 hostname: host.to_string(),
464 key_type: remote_key_type,
465 remote_host_key: remote_host_key_encoded,
466 location: known_host.location.clone(),
467 });
468 }
469 }
470 KnownHostLineType::CertAuthority => {
471 latent_errors.push(KnownHostError::HostHasOnlyCertAuthority {
473 hostname: host.to_string(),
474 location: known_host.location.clone(),
475 });
476 }
477 }
478 }
479
480 if accepted_known_host_found {
482 return Ok(());
483 }
484
485 if latent_errors.is_empty() {
486 Err(KnownHostError::HostKeyNotFound {
491 hostname: host.to_string(),
492 key_type: remote_key_type,
493 remote_host_key: remote_host_key_encoded,
494 remote_fingerprint,
495 other_hosts,
496 })
497 } else {
498 if let Some(index) = latent_errors
502 .iter()
503 .position(|e| matches!(e, KnownHostError::HostKeyHasChanged { .. }))
504 {
505 return Err(latent_errors.remove(index));
506 } else {
507 Err(latent_errors.pop().unwrap())
510 }
511 }
512}
513
514fn known_host_files(gctx: &GlobalContext) -> Vec<PathBuf> {
516 let mut result = Vec::new();
517 if gctx
518 .get_env_os("__CARGO_TEST_DISABLE_GLOBAL_KNOWN_HOST")
519 .is_some()
520 {
521 } else if cfg!(unix) {
522 result.push(PathBuf::from("/etc/ssh/ssh_known_hosts"));
523 } else if cfg!(windows) {
524 if let Some(progdata) = gctx.get_env_os("ProgramData") {
530 let mut progdata = PathBuf::from(progdata);
531 progdata.push("ssh");
532 progdata.push("ssh_known_hosts");
533 result.push(progdata)
534 }
535 }
536 result.extend(user_known_host_location());
537 result
538}
539
540fn user_known_host_location() -> Option<PathBuf> {
542 home::home_dir().map(|mut home| {
560 home.push(".ssh");
561 home.push("known_hosts");
562 home
563 })
564}
565
566fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
569 let user = user_known_host_location();
571 let openssh_loc = match &user {
572 Some(path) => path.to_str().expect("utf-8 home"),
573 None => "~/.ssh/known_hosts",
574 };
575 format!(
576 "the `net.ssh.known-hosts` array in your Cargo configuration \
577 (such as {diagnostic_home_config}) \
578 or in your OpenSSH known_hosts file at {openssh_loc}"
579 )
580}
581
582const HASH_HOSTNAME_PREFIX: &str = "|1|";
583
584#[derive(Clone)]
585enum KnownHostLineType {
586 Key,
587 CertAuthority,
588 Revoked,
589}
590
591#[derive(Clone)]
593struct KnownHost {
594 location: KnownHostLocation,
595 patterns: String,
597 key_type: String,
598 key: Vec<u8>,
599 line_type: KnownHostLineType,
600}
601
602impl KnownHost {
603 fn host_matches(&self, host: &str) -> bool {
605 let mut match_found = false;
606 let host = host.to_lowercase();
607 if let Some(hashed) = self.patterns.strip_prefix(HASH_HOSTNAME_PREFIX) {
608 return hashed_hostname_matches(&host, hashed);
609 }
610 for pattern in self.patterns.split(',') {
611 let pattern = pattern.to_lowercase();
612 let is_glob = is_glob_pattern(&pattern);
613
614 if is_glob {
615 match glob::Pattern::new(&pattern) {
616 Ok(glob) => match_found |= glob.matches(&host),
617 Err(e) => {
618 tracing::warn!(
619 "failed to interpret hostname `{pattern}` as glob pattern: {e}"
620 )
621 }
622 }
623 }
624
625 if let Some(pattern) = pattern.strip_prefix('!') {
626 if pattern == host {
627 return false;
628 }
629 } else {
630 match_found |= pattern == host;
631 }
632 }
633 match_found
634 }
635}
636
637fn hashed_hostname_matches(host: &str, hashed: &str) -> bool {
638 let Some((b64_salt, b64_host)) = hashed.split_once('|') else {
639 return false;
640 };
641 let Ok(salt) = STANDARD.decode(b64_salt) else {
642 return false;
643 };
644 let Ok(hashed_host) = STANDARD.decode(b64_host) else {
645 return false;
646 };
647 let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt) else {
648 return false;
649 };
650 mac.update(host.as_bytes());
651 let result = mac.finalize().into_bytes();
652 hashed_host == &result[..]
653}
654
655fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
657 let contents = cargo_util::paths::read(path)?;
658 Ok(load_hostfile_contents(path, &contents))
659}
660
661fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> {
662 let entries = contents
663 .lines()
664 .enumerate()
665 .filter_map(|(lineno, line)| {
666 let location = KnownHostLocation::File {
667 path: path.to_path_buf(),
668 lineno: lineno as u32 + 1,
669 };
670 parse_known_hosts_line(line, location)
671 })
672 .collect();
673 entries
674}
675
676fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> {
677 let line = line.trim();
678 if line.is_empty() || line.starts_with('#') {
679 return None;
680 }
681 let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty());
682
683 let line_type = if line.starts_with("@") {
684 let line_type = parts.next()?;
685
686 if line_type == "@cert-authority" {
687 KnownHostLineType::CertAuthority
688 } else if line_type == "@revoked" {
689 KnownHostLineType::Revoked
690 } else {
691 return None;
693 }
694 } else {
695 KnownHostLineType::Key
696 };
697
698 let patterns = parts.next()?;
699 let key_type = parts.next()?;
700 let key = parts.next().map(|p| STANDARD.decode(p))?.ok()?;
701 Some(KnownHost {
702 line_type,
703 location,
704 patterns: patterns.to_string(),
705 key_type: key_type.to_string(),
706 key,
707 })
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 static COMMON_CONTENTS: &str = r#"
715 # Comments allowed at start of line
716
717 example.com,rust-lang.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5MzWIpZwpkpDjyCNiTIEVFhSA9OUUQvjFo7CgZBGCAj/cqeUIgiLsgtfmtBsfWIkAECQpM7ePP7NLZFGJcHvoyg5jXJiIX5s0eKo9IlcuTLLrMkW5MkHXE7bNklVbW1WdCfF2+y7Ao25B4L8FFRokMh0yp/H6+8xZ7PdVwL3FRPEg8ftZ5R0kuups6xiMHPRX+f/07vfJzA47YDPmXfhkn+JK8kL0JYw8iy8BtNBfRQL99d9iXJzWXnNce5NHMuKD5rOonD3aQHLDlwK+KhrFRrdaxQEM8ZWxNti0ux8yT4Dl5jJY0CrIu3Xl6+qroVgTqJGNkTbhs5DGWdFh6BLPTTH15rN4buisg7uMyLyHqx06ckborqD33gWu+Jig7O+PV6KJmL5mp1O1HXvZqkpBdTiT6GiDKG3oECCIXkUk0BSU9VG9VQcrMxxvgiHlyoXUAfYQoXv/lnxkTnm+Sr36kutsVOs7n5B43ZKAeuaxyQ11huJZpxamc0RA1HM641s= eric@host
718 Example.net ssh-dss AAAAB3NzaC1kc3MAAACBAK2Ek3jVxisXmz5UcZ7W65BAj/nDJCCVvSe0Aytndn4PH6k7sVesut5OoY6PdksZ9tEfuFjjS9HR5SJb8j1GW0GxtaSHHbf+rNc36PeU75bffzyIWwpA8uZFONt5swUAXJXcsHOoapNbUFuhHsRhB2hXxz9QGNiiwIwRJeSHixKRAAAAFQChKfxO1z9H2/757697xP5nJ/Z5dwAAAIEAoc+HIWas+4WowtB/KtAp6XE0B9oHI+55wKtdcGwwb7zHKK9scWNXwxIcMhSvyB3Oe2I7dQQlvyIWxsdZlzOkX0wdsTHjIAnBAP68MyvMv4kq3+I5GAVcFsqoLZfZvh0dlcgUq1/YNYZwKlt89tnzk8Fp4KLWmuw8Bd8IShYVa78AAACAL3qd8kNTY7CthgsQ8iWdjbkGSF/1KCeFyt8UjurInp9wvPDjqagwakbyLOzN7y3/ItTPCaGuX+RjFP0zZTf8i9bsAVyjFJiJ7vzRXcWytuFWANrpzLTn1qzPfh63iK92Aw8AVBYvEA/4bxo+XReAvhNBB/m78G6OedTeu6ZoTsI= eric@host
719 [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
720 nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host
721 nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host
722 nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host
723 # Revoked is supported, but without Cert-Authority support, it will only negate some other fixed key.
724 @revoked revoked.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
725 # Cert-Authority is not supported (below key should not be valid anyway)
726 @cert-authority ca.example.com ssh-rsa AABBB5Wm
727 example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
728 192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
729 |1|QxzZoTXIWLhUsuHAXjuDMIV3FjQ=|M6NCOIkjiWdCWqkh5+Q+/uFLGjs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
730 # Negation isn't terribly useful without globs.
731 neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host
732 # Glob patterns
733 *.asterisk.glob.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6/wm8Z5aVL2cDyALY6zE7KVW0s64utWTUmbAvvSKlI eric@host
734 test?.question.glob.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKceiey2vuK/WB/kLsiGa85xw897JzvGGaHmkAZbVHf3 eric@host
735 "#;
736
737 #[test]
738 fn known_hosts_parse() {
739 let kh_path = Path::new("/home/abc/.known_hosts");
740 let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
741 assert_eq!(khs.len(), 14);
742 match &khs[0].location {
743 KnownHostLocation::File { path, lineno } => {
744 assert_eq!(path, kh_path);
745 assert_eq!(*lineno, 4);
746 }
747 _ => panic!("unexpected"),
748 }
749 assert_eq!(khs[0].patterns, "example.com,rust-lang.org");
750 assert_eq!(khs[0].key_type, "ssh-rsa");
751 assert_eq!(khs[0].key.len(), 407);
752 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)");
753 match &khs[1].location {
754 KnownHostLocation::File { path, lineno } => {
755 assert_eq!(path, kh_path);
756 assert_eq!(*lineno, 5);
757 }
758 _ => panic!("unexpected"),
759 }
760 assert_eq!(khs[2].patterns, "[example.net]:2222");
761 assert_eq!(khs[3].patterns, "nistp256.example.org");
762 assert_eq!(khs[9].patterns, "192.168.42.12");
763 }
764
765 #[test]
766 fn host_matches() {
767 let kh_path = Path::new("/home/abc/.known_hosts");
768 let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
769 assert!(khs[0].host_matches("example.com"));
770 assert!(khs[0].host_matches("rust-lang.org"));
771 assert!(khs[0].host_matches("EXAMPLE.COM"));
772 assert!(khs[1].host_matches("example.net"));
773 assert!(!khs[0].host_matches("example.net"));
774 assert!(khs[2].host_matches("[example.net]:2222"));
775 assert!(!khs[2].host_matches("example.net"));
776 assert!(khs[10].host_matches("hashed.example.com"));
777 assert!(!khs[10].host_matches("example.com"));
778 assert!(!khs[11].host_matches("neg.example.com"));
779
780 assert!(khs[12].host_matches("matches.asterisk.glob.example.com"));
782 assert!(!khs[12].host_matches("matches.not.glob.example.com"));
783 assert!(khs[13].host_matches("test3.question.glob.example.com"));
784 assert!(!khs[13].host_matches("test120.question.glob.example.com"));
785 }
786
787 #[test]
788 fn check_match() {
789 let kh_path = Path::new("/home/abc/.known_hosts");
790 let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
791
792 assert!(
793 check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Rsa, &khs[0].key)
794 .is_ok()
795 );
796
797 match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Dss, &khs[0].key) {
798 Err(KnownHostError::HostKeyNotFound {
799 hostname,
800 remote_fingerprint,
801 other_hosts,
802 ..
803 }) => {
804 assert_eq!(
805 remote_fingerprint,
806 "yn+pONDn0EcgdOCVptgB4RZd/wqmsVKrPnQMLtrvhw8"
807 );
808 assert_eq!(hostname, "example.com");
809 assert_eq!(other_hosts.len(), 0);
810 }
811 _ => panic!("unexpected"),
812 }
813
814 match check_ssh_known_hosts_loaded(
815 &khs,
816 "foo.example.com",
817 SshHostKeyType::Rsa,
818 &khs[0].key,
819 ) {
820 Err(KnownHostError::HostKeyNotFound { other_hosts, .. }) => {
821 assert_eq!(other_hosts.len(), 1);
822 assert_eq!(other_hosts[0].patterns, "example.com,rust-lang.org");
823 }
824 _ => panic!("unexpected"),
825 }
826
827 let mut modified_key = khs[0].key.clone();
828 modified_key[0] = 1;
829 match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Rsa, &modified_key)
830 {
831 Err(KnownHostError::HostKeyHasChanged { old_known_host, .. }) => {
832 assert!(matches!(
833 old_known_host.location,
834 KnownHostLocation::File { lineno: 4, .. }
835 ));
836 }
837 _ => panic!("unexpected"),
838 }
839 }
840
841 #[test]
842 fn revoked() {
843 let kh_path = Path::new("/home/abc/.known_hosts");
844 let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
845
846 match check_ssh_known_hosts_loaded(
847 &khs,
848 "revoked.example.com",
849 SshHostKeyType::Ed255219,
850 &khs[6].key,
851 ) {
852 Err(KnownHostError::HostKeyRevoked {
853 hostname, location, ..
854 }) => {
855 assert_eq!("revoked.example.com", hostname);
856 assert!(matches!(
857 location,
858 KnownHostLocation::File { lineno: 11, .. }
859 ));
860 }
861 _ => panic!("Expected key to be revoked for revoked.example.com."),
862 }
863 }
864
865 #[test]
866 fn cert_authority() {
867 let kh_path = Path::new("/home/abc/.known_hosts");
868 let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
869
870 match check_ssh_known_hosts_loaded(
871 &khs,
872 "ca.example.com",
873 SshHostKeyType::Rsa,
874 &khs[0].key, ) {
876 Err(KnownHostError::HostHasOnlyCertAuthority {
877 hostname, location, ..
878 }) => {
879 assert_eq!("ca.example.com", hostname);
880 assert!(matches!(
881 location,
882 KnownHostLocation::File { lineno: 13, .. }
883 ));
884 }
885 Err(KnownHostError::HostKeyNotFound { hostname, .. }) => {
886 panic!("host key not found... {}", hostname);
887 }
888 _ => panic!("Expected host to only have @cert-authority line (which is unsupported)."),
889 }
890 }
891
892 #[test]
893 fn multiple_errors() {
894 let contents = r#"
895 not-used.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
896 # Cert-authority and changed key for the same host - changed key error should prevail
897 @cert-authority example.com ssh-ed25519 AABBB5Wm
898 example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
899 "#;
900
901 let kh_path = Path::new("/home/abc/.known_hosts");
902 let khs = load_hostfile_contents(kh_path, contents);
903
904 match check_ssh_known_hosts_loaded(
905 &khs,
906 "example.com",
907 SshHostKeyType::Ed255219,
908 &khs[0].key,
909 ) {
910 Err(KnownHostError::HostKeyHasChanged {
911 hostname,
912 old_known_host,
913 remote_host_key,
914 ..
915 }) => {
916 assert_eq!("example.com", hostname);
917 assert_eq!(
918 "AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY",
919 remote_host_key
920 );
921 assert!(matches!(
922 old_known_host.location,
923 KnownHostLocation::File { lineno: 5, .. }
924 ));
925 }
926 _ => panic!("Expected error to be of type HostKeyHasChanged."),
927 }
928 }
929
930 #[test]
931 fn known_host_and_revoked() {
932 let contents = r#"
933 example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
934 # Later in the file the same host key is revoked
935 @revoked example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
936 "#;
937
938 let kh_path = Path::new("/home/abc/.known_hosts");
939 let khs = load_hostfile_contents(kh_path, contents);
940
941 match check_ssh_known_hosts_loaded(
942 &khs,
943 "example.com",
944 SshHostKeyType::Ed255219,
945 &khs[0].key,
946 ) {
947 Err(KnownHostError::HostKeyRevoked {
948 hostname,
949 remote_host_key,
950 location,
951 ..
952 }) => {
953 assert_eq!("example.com", hostname);
954 assert_eq!(
955 "AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR",
956 remote_host_key
957 );
958 assert!(matches!(
959 location,
960 KnownHostLocation::File { lineno: 4, .. }
961 ));
962 }
963 _ => panic!("Expected host key to be reject with error HostKeyRevoked."),
964 }
965 }
966}