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