Skip to content

Commit 026bda3

Browse files
ehusspietroalbini
authored andcommitted
Support configuring ssh known-hosts via cargo config.
1 parent 9f62f84 commit 026bda3

File tree

5 files changed

+119
-12
lines changed

5 files changed

+119
-12
lines changed

src/cargo/sources/git/known_hosts.rs

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
//! hostname patterns, and revoked markers. See "FIXME" comments littered in
2121
//! this file.
2222
23+
use crate::util::config::{Definition, Value};
2324
use git2::cert::Cert;
2425
use git2::CertificateCheckStatus;
2526
use std::collections::HashSet;
@@ -74,6 +75,8 @@ impl From<anyhow::Error> for KnownHostError {
7475
enum KnownHostLocation {
7576
/// Loaded from a file from disk.
7677
File { path: PathBuf, lineno: u32 },
78+
/// Loaded from cargo's config system.
79+
Config { definition: Definition },
7780
/// Part of the hard-coded bundled keys in Cargo.
7881
Bundled,
7982
}
@@ -83,6 +86,8 @@ pub fn certificate_check(
8386
cert: &Cert<'_>,
8487
host: &str,
8588
port: Option<u16>,
89+
config_known_hosts: Option<&Vec<Value<String>>>,
90+
diagnostic_home_config: &str,
8691
) -> Result<CertificateCheckStatus, git2::Error> {
8792
let Some(host_key) = cert.as_hostkey() else {
8893
// Return passthrough for TLS X509 certificates to use whatever validation
@@ -96,7 +101,7 @@ pub fn certificate_check(
96101
_ => host.to_string(),
97102
};
98103
// The error message must be constructed as a string to pass through the libgit2 C API.
99-
let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port) {
104+
let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port, config_known_hosts) {
100105
Ok(()) => {
101106
return Ok(CertificateCheckStatus::CertificateOk);
102107
}
@@ -113,13 +118,13 @@ pub fn certificate_check(
113118
// Try checking without the port.
114119
if port.is_some()
115120
&& !matches!(port, Some(22))
116-
&& check_ssh_known_hosts(host_key, host).is_ok()
121+
&& check_ssh_known_hosts(host_key, host, config_known_hosts).is_ok()
117122
{
118123
return Ok(CertificateCheckStatus::CertificateOk);
119124
}
120125
let key_type_short_name = key_type.short_name();
121126
let key_type_name = key_type.name();
122-
let known_hosts_location = user_known_host_location_to_add();
127+
let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
123128
let other_hosts_message = if other_hosts.is_empty() {
124129
String::new()
125130
} else {
@@ -132,6 +137,9 @@ pub fn certificate_check(
132137
KnownHostLocation::File { path, lineno } => {
133138
format!("{} line {lineno}", path.display())
134139
}
140+
KnownHostLocation::Config { definition } => {
141+
format!("config value from {definition}")
142+
}
135143
KnownHostLocation::Bundled => format!("bundled with cargo"),
136144
};
137145
write!(msg, " {loc}: {}\n", known_host.patterns).unwrap();
@@ -163,7 +171,7 @@ pub fn certificate_check(
163171
}) => {
164172
let key_type_short_name = key_type.short_name();
165173
let key_type_name = key_type.name();
166-
let known_hosts_location = user_known_host_location_to_add();
174+
let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config);
167175
let old_key_resolution = match old_known_host.location {
168176
KnownHostLocation::File { path, lineno } => {
169177
let old_key_location = path.display();
@@ -173,6 +181,13 @@ pub fn certificate_check(
173181
and adding the new key to {known_hosts_location}",
174182
)
175183
}
184+
KnownHostLocation::Config { definition } => {
185+
format!(
186+
"removing the old {key_type_name} key for `{hostname}` \
187+
loaded from Cargo's config at {definition}, \
188+
and adding the new key to {known_hosts_location}"
189+
)
190+
}
176191
KnownHostLocation::Bundled => {
177192
format!(
178193
"adding the new key to {known_hosts_location}\n\
@@ -217,6 +232,7 @@ pub fn certificate_check(
217232
fn check_ssh_known_hosts(
218233
cert_host_key: &git2::cert::CertHostkey<'_>,
219234
host: &str,
235+
config_known_hosts: Option<&Vec<Value<String>>>,
220236
) -> Result<(), KnownHostError> {
221237
let Some(remote_host_key) = cert_host_key.hostkey() else {
222238
return Err(anyhow::format_err!("remote host key is not available").into());
@@ -237,6 +253,23 @@ fn check_ssh_known_hosts(
237253
let hosts = load_hostfile(&path)?;
238254
known_hosts.extend(hosts);
239255
}
256+
if let Some(config_known_hosts) = config_known_hosts {
257+
// Format errors aren't an error in case the format needs to change in
258+
// the future, to retain forwards compatibility.
259+
for line_value in config_known_hosts {
260+
let location = KnownHostLocation::Config {
261+
definition: line_value.definition.clone(),
262+
};
263+
match parse_known_hosts_line(&line_value.val, location) {
264+
Some(known_host) => known_hosts.push(known_host),
265+
None => log::warn!(
266+
"failed to parse known host {} from {}",
267+
line_value.val,
268+
line_value.definition
269+
),
270+
}
271+
}
272+
}
240273
// Load the bundled keys. Don't add keys for hosts that the user has
241274
// configured, which gives them the option to override them. This could be
242275
// useful if the keys are ever revoked.
@@ -363,12 +396,18 @@ fn user_known_host_location() -> Option<PathBuf> {
363396

364397
/// The location to display in an error message instructing the user where to
365398
/// add the new key.
366-
fn user_known_host_location_to_add() -> String {
399+
fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
367400
// Note that we don't bother with the legacy known_hosts2 files.
368-
match user_known_host_location() {
369-
Some(path) => path.to_str().expect("utf-8 home").to_string(),
370-
None => "~/.ssh/known_hosts".to_string(),
371-
}
401+
let user = user_known_host_location();
402+
let openssh_loc = match &user {
403+
Some(path) => path.to_str().expect("utf-8 home"),
404+
None => "~/.ssh/known_hosts",
405+
};
406+
format!(
407+
"the `net.ssh.known-hosts` array in your Cargo configuration \
408+
(such as {diagnostic_home_config}) \
409+
or in your OpenSSH known_hosts file at {openssh_loc}"
410+
)
372411
}
373412

374413
/// A single known host entry.

src/cargo/sources/git/utils.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,9 @@ pub fn with_fetch_options(
726726
cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>,
727727
) -> CargoResult<()> {
728728
let mut progress = Progress::new("Fetch", config);
729+
let ssh_config = config.net_config()?.ssh.as_ref();
730+
let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref());
731+
let diagnostic_home_config = config.diagnostic_home_config();
729732
network::with_retry(config, || {
730733
with_authentication(url, git_config, |f| {
731734
let port = Url::parse(url).ok().and_then(|url| url.port());
@@ -736,7 +739,13 @@ pub fn with_fetch_options(
736739
let mut counter = MetricsCounter::<10>::new(0, last_update);
737740
rcb.credentials(f);
738741
rcb.certificate_check(|cert, host| {
739-
super::known_hosts::certificate_check(cert, host, port)
742+
super::known_hosts::certificate_check(
743+
cert,
744+
host,
745+
port,
746+
config_known_hosts,
747+
&diagnostic_home_config,
748+
)
740749
});
741750
rcb.transfer_progress(|stats| {
742751
let indexed_deltas = stats.indexed_deltas();

src/cargo/util/config/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,18 @@ impl Config {
356356
&self.home_path
357357
}
358358

359+
/// Returns a path to display to the user with the location of their home
360+
/// config file (to only be used for displaying a diagnostics suggestion,
361+
/// such as recommending where to add a config value).
362+
pub fn diagnostic_home_config(&self) -> String {
363+
let home = self.home_path.as_path_unlocked();
364+
let path = match self.get_file_path(home, "config", false) {
365+
Ok(Some(existing_path)) => existing_path,
366+
_ => home.join("config.toml"),
367+
};
368+
path.to_string_lossy().to_string()
369+
}
370+
359371
/// Gets the Cargo Git directory (`<cargo_home>/git`).
360372
pub fn git_path(&self) -> Filesystem {
361373
self.home_path.join("git")
@@ -2356,6 +2368,13 @@ pub struct CargoNetConfig {
23562368
pub retry: Option<u32>,
23572369
pub offline: Option<bool>,
23582370
pub git_fetch_with_cli: Option<bool>,
2371+
pub ssh: Option<CargoSshConfig>,
2372+
}
2373+
2374+
#[derive(Debug, Deserialize)]
2375+
#[serde(rename_all = "kebab-case")]
2376+
pub struct CargoSshConfig {
2377+
pub known_hosts: Option<Vec<Value<String>>>,
23592378
}
23602379

23612380
#[derive(Debug, Deserialize)]

src/doc/src/appendix/git-authentication.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ known hosts in OpenSSH-style `known_hosts` files located in their standard
6666
locations (`.ssh/known_hosts` in your home directory, or
6767
`/etc/ssh/ssh_known_hosts` on Unix-like platforms or
6868
`%PROGRAMDATA%\ssh\ssh_known_hosts` on Windows). More information about these
69-
files can be found in the [sshd man page].
69+
files can be found in the [sshd man page]. Alternatively, keys may be
70+
configured in a Cargo configuration file with [`net.ssh.known-hosts`].
7071

7172
When connecting to an SSH host before the known hosts has been configured,
7273
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
7879
<https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>.
7980

8081
Cargo comes with the host keys for [github.com](https://github.com) built-in.
81-
If those ever change, you can add the new keys to your known_hosts file.
82+
If those ever change, you can add the new keys to the config or known_hosts file.
8283

8384
[`credential.helper`]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage
8485
[`net.git-fetch-with-cli`]: ../reference/config.md#netgit-fetch-with-cli
86+
[`net.ssh.known-hosts`]: ../reference/config.md#netsshknown-hosts
8587
[GCM]: https://github.com/microsoft/Git-Credential-Manager-Core/
8688
[PuTTY]: https://www.chiark.greenend.org.uk/~sgtatham/putty/
8789
[Microsoft installation documentation]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse

src/doc/src/reference/config.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ retry = 2 # network retries
114114
git-fetch-with-cli = true # use the `git` executable for git operations
115115
offline = true # do not access the network
116116

117+
[net.ssh]
118+
known-hosts = ["..."] # known SSH host keys
119+
117120
[patch.<registry>]
118121
# Same keys as for [patch] in Cargo.toml
119122

@@ -750,6 +753,41 @@ needed, and generate an error if it encounters a network error.
750753

751754
Can be overridden with the `--offline` command-line option.
752755

756+
##### `net.ssh`
757+
758+
The `[net.ssh]` table contains settings for SSH connections.
759+
760+
##### `net.ssh.known-hosts`
761+
* Type: array of strings
762+
* Default: see description
763+
* Environment: not supported
764+
765+
The `known-hosts` array contains a list of SSH host keys that should be
766+
accepted as valid when connecting to an SSH server (such as for SSH git
767+
dependencies). Each entry should be a string in a format similar to OpenSSH
768+
`known_hosts` files. Each string should start with one or more hostnames
769+
separated by commas, a space, the key type name, a space, and the
770+
base64-encoded key. For example:
771+
772+
```toml
773+
[net.ssh]
774+
known-hosts = [
775+
"example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFO4Q5T0UV0SQevair9PFwoxY9dl4pQl3u5phoqJH3cF"
776+
]
777+
```
778+
779+
Cargo will attempt to load known hosts keys from common locations supported in
780+
OpenSSH, and will join those with any listed in a Cargo configuration file.
781+
If any matching entry has the correct key, the connection will be allowed.
782+
783+
Cargo comes with the host keys for [github.com][github-keys] built-in. If
784+
those ever change, you can add the new keys to the config or known_hosts file.
785+
786+
See [Git Authentication](../appendix/git-authentication.md#ssh-known-hosts)
787+
for more details.
788+
789+
[github-keys]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
790+
753791
#### `[patch]`
754792

755793
Just as you can override dependencies using [`[patch]` in

0 commit comments

Comments
 (0)