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