commit 026bda3fb5eddac0df111ee150706f756558a7b3
Author: Eric Huss <eric@huss.org>
Date:   Fri Dec 9 20:38:12 2022 -0800

    Support configuring ssh known-hosts via cargo config.

diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs
index 875dcf63f3..7efea43c3b 100644
--- a/src/cargo/sources/git/known_hosts.rs
+++ b/src/cargo/sources/git/known_hosts.rs
@@ -20,6 +20,7 @@
 //! hostname patterns, and revoked markers. See "FIXME" comments littered in
 //! this file.
 
+use crate::util::config::{Definition, Value};
 use git2::cert::Cert;
 use git2::CertificateCheckStatus;
 use std::collections::HashSet;
@@ -74,6 +75,8 @@ impl From<anyhow::Error> for KnownHostError {
 enum KnownHostLocation {
     /// Loaded from a file from disk.
     File { path: PathBuf, lineno: u32 },
+    /// Loaded from cargo's config system.
+    Config { definition: Definition },
     /// Part of the hard-coded bundled keys in Cargo.
     Bundled,
 }
@@ -83,6 +86,8 @@ pub fn certificate_check(
     cert: &Cert<'_>,
     host: &str,
     port: Option<u16>,
+    config_known_hosts: Option<&Vec<Value<String>>>,
+    diagnostic_home_config: &str,
 ) -> Result<CertificateCheckStatus, git2::Error> {
     let Some(host_key) = cert.as_hostkey() else {
         // Return passthrough for TLS X509 certificates to use whatever validation
@@ -96,7 +101,7 @@ pub fn certificate_check(
         _ => host.to_string(),
     };
     // The error message must be constructed as a string to pass through the libgit2 C API.
-    let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port) {
+    let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port, config_known_hosts) {
         Ok(()) => {
             return Ok(CertificateCheckStatus::CertificateOk);
         }
@@ -113,13 +118,13 @@ pub fn certificate_check(
             // Try checking without the port.
             if port.is_some()
                 && !matches!(port, Some(22))
-                && check_ssh_known_hosts(host_key, host).is_ok()
+                && check_ssh_known_hosts(host_key, host, config_known_hosts).is_ok()
             {
                 return Ok(CertificateCheckStatus::CertificateOk);
             }
             let key_type_short_name = key_type.short_name();
             let key_type_name = key_type.name();
-            let known_hosts_location = user_known_host_location_to_add();
+            let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
             let other_hosts_message = if other_hosts.is_empty() {
                 String::new()
             } else {
@@ -132,6 +137,9 @@ pub fn certificate_check(
                         KnownHostLocation::File { path, lineno } => {
                             format!("{} line {lineno}", path.display())
                         }
+                        KnownHostLocation::Config { definition } => {
+                            format!("config value from {definition}")
+                        }
                         KnownHostLocation::Bundled => format!("bundled with cargo"),
                     };
                     write!(msg, "    {loc}: {}\n", known_host.patterns).unwrap();
@@ -163,7 +171,7 @@ pub fn certificate_check(
         }) => {
             let key_type_short_name = key_type.short_name();
             let key_type_name = key_type.name();
-            let known_hosts_location = user_known_host_location_to_add();
+            let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
             let old_key_resolution = match old_known_host.location {
                 KnownHostLocation::File { path, lineno } => {
                     let old_key_location = path.display();
@@ -173,6 +181,13 @@ pub fn certificate_check(
                         and adding the new key to {known_hosts_location}",
                     )
                 }
+                KnownHostLocation::Config { definition } => {
+                    format!(
+                        "removing the old {key_type_name} key for `{hostname}` \
+                        loaded from Cargo's config at {definition}, \
+                        and adding the new key to {known_hosts_location}"
+                    )
+                }
                 KnownHostLocation::Bundled => {
                     format!(
                         "adding the new key to {known_hosts_location}\n\
@@ -217,6 +232,7 @@ pub fn certificate_check(
 fn check_ssh_known_hosts(
     cert_host_key: &git2::cert::CertHostkey<'_>,
     host: &str,
+    config_known_hosts: Option<&Vec<Value<String>>>,
 ) -> Result<(), KnownHostError> {
     let Some(remote_host_key) = cert_host_key.hostkey() else {
         return Err(anyhow::format_err!("remote host key is not available").into());
@@ -237,6 +253,23 @@ fn check_ssh_known_hosts(
         let hosts = load_hostfile(&path)?;
         known_hosts.extend(hosts);
     }
+    if let Some(config_known_hosts) = config_known_hosts {
+        // Format errors aren't an error in case the format needs to change in
+        // the future, to retain forwards compatibility.
+        for line_value in config_known_hosts {
+            let location = KnownHostLocation::Config {
+                definition: line_value.definition.clone(),
+            };
+            match parse_known_hosts_line(&line_value.val, location) {
+                Some(known_host) => known_hosts.push(known_host),
+                None => log::warn!(
+                    "failed to parse known host {} from {}",
+                    line_value.val,
+                    line_value.definition
+                ),
+            }
+        }
+    }
     // Load the bundled keys. Don't add keys for hosts that the user has
     // configured, which gives them the option to override them. This could be
     // useful if the keys are ever revoked.
@@ -363,12 +396,18 @@ fn user_known_host_location() -> Option<PathBuf> {
 
 /// The location to display in an error message instructing the user where to
 /// add the new key.
-fn user_known_host_location_to_add() -> String {
+fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
     // Note that we don't bother with the legacy known_hosts2 files.
-    match user_known_host_location() {
-        Some(path) => path.to_str().expect("utf-8 home").to_string(),
-        None => "~/.ssh/known_hosts".to_string(),
-    }
+    let user = user_known_host_location();
+    let openssh_loc = match &user {
+        Some(path) => path.to_str().expect("utf-8 home"),
+        None => "~/.ssh/known_hosts",
+    };
+    format!(
+        "the `net.ssh.known-hosts` array in your Cargo configuration \
+        (such as {diagnostic_home_config}) \
+        or in your OpenSSH known_hosts file at {openssh_loc}"
+    )
 }
 
 /// A single known host entry.
diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs
index 831c43be6b..457c97c5bb 100644
--- a/src/cargo/sources/git/utils.rs
+++ b/src/cargo/sources/git/utils.rs
@@ -726,6 +726,9 @@ pub fn with_fetch_options(
     cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
 ) -> CargoResult<()> {
     let mut progress = Progress::new("Fetch", config);
+    let ssh_config = config.net_config()?.ssh.as_ref();
+    let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
+    let diagnostic_home_config = config.diagnostic_home_config();
     network::with_retry(config, || {
         with_authentication(url, git_config, |f| {
             let port = Url::parse(url).ok().and_then(|url| url.port());
@@ -736,7 +739,13 @@ pub fn with_fetch_options(
             let mut counter = MetricsCounter::<10>::new(0, last_update);
             rcb.credentials(f);
             rcb.certificate_check(|cert, host| {
-                super::known_hosts::certificate_check(cert, host, port)
+                super::known_hosts::certificate_check(
+                    cert,
+                    host,
+                    port,
+                    config_known_hosts,
+                    &diagnostic_home_config,
+                )
             });
             rcb.transfer_progress(|stats| {
                 let indexed_deltas = stats.indexed_deltas();
diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs
index d30e094413..d9ab142c4e 100644
--- a/src/cargo/util/config/mod.rs
+++ b/src/cargo/util/config/mod.rs
@@ -356,6 +356,18 @@ impl Config {
         &self.home_path
     }
 
+    /// Returns a path to display to the user with the location of their home
+    /// config file (to only be used for displaying a diagnostics suggestion,
+    /// such as recommending where to add a config value).
+    pub fn diagnostic_home_config(&self) -> String {
+        let home = self.home_path.as_path_unlocked();
+        let path = match self.get_file_path(home, "config", false) {
+            Ok(Some(existing_path)) => existing_path,
+            _ => home.join("config.toml"),
+        };
+        path.to_string_lossy().to_string()
+    }
+
     /// Gets the Cargo Git directory (`<cargo_home>/git`).
     pub fn git_path(&self) -> Filesystem {
         self.home_path.join("git")
@@ -2356,6 +2368,13 @@ pub struct CargoNetConfig {
     pub retry: Option<u32>,
     pub offline: Option<bool>,
     pub git_fetch_with_cli: Option<bool>,
+    pub ssh: Option<CargoSshConfig>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct CargoSshConfig {
+    pub known_hosts: Option<Vec<Value<String>>>,
 }
 
 #[derive(Debug, Deserialize)]
diff --git a/src/doc/src/appendix/git-authentication.md b/src/doc/src/appendix/git-authentication.md
index a7db1ac7f1..f46a6535c6 100644
--- a/src/doc/src/appendix/git-authentication.md
+++ b/src/doc/src/appendix/git-authentication.md
@@ -66,7 +66,8 @@ known hosts in OpenSSH-style `known_hosts` files located in their standard
 locations (`.ssh/known_hosts` in your home directory, or
 `/etc/ssh/ssh_known_hosts` on Unix-like platforms or
 `%PROGRAMDATA%\ssh\ssh_known_hosts` on Windows). More information about these
-files can be found in the [sshd man page].
+files can be found in the [sshd man page]. Alternatively, keys may be
+configured in a Cargo configuration file with [`net.ssh.known-hosts`].
 
 When connecting to an SSH host before the known hosts has been configured,
 Cargo will display an error message instructing you how to add the host key.
@@ -78,10 +79,11 @@ publish their fingerprints on the web; for example GitHub posts theirs at
 <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>.
 
 Cargo comes with the host keys for [github.com](https://github.com) built-in.
-If those ever change, you can add the new keys to your known_hosts file.
+If those ever change, you can add the new keys to the config or known_hosts file.
 
 [`credential.helper`]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage
 [`net.git-fetch-with-cli`]: ../reference/config.md#netgit-fetch-with-cli
+[`net.ssh.known-hosts`]: ../reference/config.md#netsshknown-hosts
 [GCM]: https://github.com/microsoft/Git-Credential-Manager-Core/
 [PuTTY]: https://www.chiark.greenend.org.uk/~sgtatham/putty/
 [Microsoft installation documentation]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse
diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md
index 1e50648797..f804ceebea 100644
--- a/src/doc/src/reference/config.md
+++ b/src/doc/src/reference/config.md
@@ -114,6 +114,9 @@ retry = 2                   # network retries
 git-fetch-with-cli = true   # use the `git` executable for git operations
 offline = true              # do not access the network
 
+[net.ssh]
+known-hosts = ["..."]       # known SSH host keys
+
 [patch.<registry>]
 # Same keys as for [patch] in Cargo.toml
 
@@ -750,6 +753,41 @@ needed, and generate an error if it encounters a network error.
 
 Can be overridden with the `--offline` command-line option.
 
+##### `net.ssh`
+
+The `[net.ssh]` table contains settings for SSH connections.
+
+##### `net.ssh.known-hosts`
+* Type: array of strings
+* Default: see description
+* Environment: not supported
+
+The `known-hosts` array contains a list of SSH host keys that should be
+accepted as valid when connecting to an SSH server (such as for SSH git
+dependencies). Each entry should be a string in a format similar to OpenSSH
+`known_hosts` files. Each string should start with one or more hostnames
+separated by commas, a space, the key type name, a space, and the
+base64-encoded key. For example:
+
+```toml
+[net.ssh]
+known-hosts = [
+    "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFO4Q5T0UV0SQevair9PFwoxY9dl4pQl3u5phoqJH3cF"
+]
+```
+
+Cargo will attempt to load known hosts keys from common locations supported in
+OpenSSH, and will join those with any listed in a Cargo configuration file.
+If any matching entry has the correct key, the connection will be allowed.
+
+Cargo comes with the host keys for [github.com][github-keys] built-in. If
+those ever change, you can add the new keys to the config or known_hosts file.
+
+See [Git Authentication](../appendix/git-authentication.md#ssh-known-hosts)
+for more details.
+
+[github-keys]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
+
 #### `[patch]`
 
 Just as you can override dependencies using [`[patch]` in
