mirror of
https://github.com/rust-lang/cargo.git
synced 2025-09-25 11:14:46 +00:00
Auto merge of #11556 - pietroalbini:cve-2022-46176, r=weihanglo
Add fix for CVE-2022-46176 r? `@ehuss`
This commit is contained in:
commit
0849a28cc2
@ -119,6 +119,13 @@
|
||||
- Added documentation of config option `registries.crates-io.protocol`.
|
||||
[#11350](https://github.com/rust-lang/cargo/pull/11350)
|
||||
|
||||
## Cargo 1.66.1 (2023-01-10)
|
||||
|
||||
### Fixed
|
||||
- [CVE-2022-46176](https://github.com/rust-lang/cargo/security/advisories/GHSA-r5w3-xm58-jv6j):
|
||||
Added validation of SSH host keys for git URLs.
|
||||
See [the docs](https://doc.rust-lang.org/cargo/appendix/git-authentication.html#ssh-known-hosts) for more information on how to configure the known host keys.
|
||||
|
||||
## Cargo 1.66 (2022-12-15)
|
||||
[08250398...rust-1.66.0](https://github.com/rust-lang/cargo/compare/08250398...rust-1.66.0)
|
||||
|
||||
|
@ -16,6 +16,7 @@ name = "cargo"
|
||||
path = "src/cargo/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.13.1"
|
||||
bytesize = "1.0"
|
||||
cargo-platform = { path = "crates/cargo-platform", version = "0.1.2" }
|
||||
cargo-util = { path = "crates/cargo-util", version = "0.2.3" }
|
||||
@ -27,10 +28,11 @@ pretty_env_logger = { version = "0.4", optional = true }
|
||||
anyhow = "1.0.47"
|
||||
filetime = "0.2.9"
|
||||
flate2 = { version = "1.0.3", default-features = false, features = ["zlib"] }
|
||||
git2 = "0.15.0"
|
||||
git2-curl = "0.16.0"
|
||||
git2 = "0.16.0"
|
||||
git2-curl = "0.17.0"
|
||||
glob = "0.3.0"
|
||||
hex = "0.4"
|
||||
hmac = "0.12.1"
|
||||
home = "0.5"
|
||||
http-auth = { version = "0.1.6", default-features = false }
|
||||
humantime = "2.0.0"
|
||||
@ -42,7 +44,7 @@ jobserver = "0.1.24"
|
||||
lazycell = "1.2.0"
|
||||
libc = "0.2"
|
||||
log = "0.4.6"
|
||||
libgit2-sys = "0.14.0"
|
||||
libgit2-sys = "0.14.1"
|
||||
memchr = "2.1.3"
|
||||
opener = "0.5"
|
||||
os_info = "3.5.0"
|
||||
@ -55,6 +57,7 @@ serde = { version = "1.0.123", features = ["derive"] }
|
||||
serde_ignored = "0.1.0"
|
||||
serde_json = { version = "1.0.30", features = ["raw_value"] }
|
||||
serde-value = "0.7.0"
|
||||
sha1 = "0.10.5"
|
||||
shell-escape = "0.1.4"
|
||||
strip-ansi-escapes = "0.1.0"
|
||||
tar = { version = "0.4.38", default-features = false }
|
||||
|
@ -17,7 +17,7 @@ filetime = "0.2"
|
||||
flate2 = { version = "1.0", default-features = false, features = ["zlib"] }
|
||||
pasetors = { version = "0.6.4", features = ["v3", "paserk", "std", "serde"] }
|
||||
time = { version = "0.3", features = ["parsing", "formatting"]}
|
||||
git2 = "0.15.0"
|
||||
git2 = "0.16.0"
|
||||
glob = "0.3"
|
||||
itertools = "0.10.0"
|
||||
lazy_static = "1.0"
|
||||
|
628
src/cargo/sources/git/known_hosts.rs
Normal file
628
src/cargo/sources/git/known_hosts.rs
Normal file
@ -0,0 +1,628 @@
|
||||
//! SSH host key validation support.
|
||||
//!
|
||||
//! A primary goal with this implementation is to provide user-friendly error
|
||||
//! messages, guiding them to understand the issue and how to resolve it.
|
||||
//!
|
||||
//! Note that there are a lot of limitations here. This reads OpenSSH
|
||||
//! known_hosts files from well-known locations, but it does not read OpenSSH
|
||||
//! config files. The config file can change the behavior of how OpenSSH
|
||||
//! handles known_hosts files. For example, some things we don't handle:
|
||||
//!
|
||||
//! - `GlobalKnownHostsFile` — Changes the location of the global host file.
|
||||
//! - `UserKnownHostsFile` — Changes the location of the user's host file.
|
||||
//! - `KnownHostsCommand` — A command to fetch known hosts.
|
||||
//! - `CheckHostIP` — DNS spoofing checks.
|
||||
//! - `VisualHostKey` — Shows a visual ascii-art key.
|
||||
//! - `VerifyHostKeyDNS` — Uses SSHFP DNS records to fetch a host key.
|
||||
//!
|
||||
//! There's also a number of things that aren't supported but could be easily
|
||||
//! added (it just adds a little complexity). For example, hostname patterns,
|
||||
//! and revoked markers. See "FIXME" comments littered in this file.
|
||||
|
||||
use crate::util::config::{Definition, Value};
|
||||
use git2::cert::{Cert, SshHostKeyType};
|
||||
use git2::CertificateCheckStatus;
|
||||
use hmac::Mac;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// These are host keys that are hard-coded in cargo to provide convenience.
|
||||
///
|
||||
/// If GitHub ever publishes new keys, the user can add them to their own
|
||||
/// configuration file to use those instead.
|
||||
///
|
||||
/// The GitHub keys are sourced from <https://api.github.com/meta> or
|
||||
/// <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>.
|
||||
///
|
||||
/// These will be ignored if the user adds their own entries for `github.com`,
|
||||
/// which can be useful if GitHub ever revokes their old keys.
|
||||
static BUNDLED_KEYS: &[(&str, &str, &str)] = &[
|
||||
("github.com", "ssh-ed25519", "AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"),
|
||||
("github.com", "ecdsa-sha2-nistp256", "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="),
|
||||
("github.com", "ssh-rsa", "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="),
|
||||
];
|
||||
|
||||
enum KnownHostError {
|
||||
/// Some general error happened while validating the known hosts.
|
||||
CheckError(anyhow::Error),
|
||||
/// The host key was not found.
|
||||
HostKeyNotFound {
|
||||
hostname: String,
|
||||
key_type: SshHostKeyType,
|
||||
remote_host_key: String,
|
||||
remote_fingerprint: String,
|
||||
other_hosts: Vec<KnownHost>,
|
||||
},
|
||||
/// The host key was found, but does not match the remote's key.
|
||||
HostKeyHasChanged {
|
||||
hostname: String,
|
||||
key_type: SshHostKeyType,
|
||||
old_known_host: KnownHost,
|
||||
remote_host_key: String,
|
||||
remote_fingerprint: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for KnownHostError {
|
||||
fn from(err: anyhow::Error) -> KnownHostError {
|
||||
KnownHostError::CheckError(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The location where a host key was located.
|
||||
#[derive(Clone)]
|
||||
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,
|
||||
}
|
||||
|
||||
/// The git2 callback used to validate a certificate (only ssh known hosts are validated).
|
||||
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
|
||||
// was done in git2.
|
||||
return Ok(CertificateCheckStatus::CertificatePassthrough)
|
||||
};
|
||||
// If a nonstandard port is in use, check for that first.
|
||||
// The fallback to check without a port is handled in the HostKeyNotFound handler.
|
||||
let host_maybe_port = match port {
|
||||
Some(port) if port != 22 => format!("[{host}]:{port}"),
|
||||
_ => 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, config_known_hosts) {
|
||||
Ok(()) => {
|
||||
return Ok(CertificateCheckStatus::CertificateOk);
|
||||
}
|
||||
Err(KnownHostError::CheckError(e)) => {
|
||||
format!("error: failed to validate host key:\n{:#}", e)
|
||||
}
|
||||
Err(KnownHostError::HostKeyNotFound {
|
||||
hostname,
|
||||
key_type,
|
||||
remote_host_key,
|
||||
remote_fingerprint,
|
||||
other_hosts,
|
||||
}) => {
|
||||
// Try checking without the port.
|
||||
if port.is_some()
|
||||
&& !matches!(port, Some(22))
|
||||
&& 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(diagnostic_home_config);
|
||||
let other_hosts_message = if other_hosts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let mut msg = String::from(
|
||||
"Note: This host key was found, \
|
||||
but is associated with a different host:\n",
|
||||
);
|
||||
for known_host in other_hosts {
|
||||
let loc = match known_host.location {
|
||||
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();
|
||||
}
|
||||
msg
|
||||
};
|
||||
format!("error: unknown SSH host key\n\
|
||||
The SSH host key for `{hostname}` is not known and cannot be validated.\n\
|
||||
\n\
|
||||
To resolve this issue, add the host key to {known_hosts_location}\n\
|
||||
\n\
|
||||
The key to add is:\n\
|
||||
\n\
|
||||
{hostname} {key_type_name} {remote_host_key}\n\
|
||||
\n\
|
||||
The {key_type_short_name} key fingerprint is: SHA256:{remote_fingerprint}\n\
|
||||
This fingerprint should be validated with the server administrator that it is correct.\n\
|
||||
{other_hosts_message}\n\
|
||||
See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
|
||||
for more information.\n\
|
||||
")
|
||||
}
|
||||
Err(KnownHostError::HostKeyHasChanged {
|
||||
hostname,
|
||||
key_type,
|
||||
old_known_host,
|
||||
remote_host_key,
|
||||
remote_fingerprint,
|
||||
}) => {
|
||||
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(diagnostic_home_config);
|
||||
let old_key_resolution = match old_known_host.location {
|
||||
KnownHostLocation::File { path, lineno } => {
|
||||
let old_key_location = path.display();
|
||||
format!(
|
||||
"removing the old {key_type_name} key for `{hostname}` \
|
||||
located at {old_key_location} line {lineno}, \
|
||||
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\
|
||||
The current host key is bundled as part of Cargo."
|
||||
)
|
||||
}
|
||||
};
|
||||
format!("error: SSH host key has changed for `{hostname}`\n\
|
||||
*********************************\n\
|
||||
* WARNING: HOST KEY HAS CHANGED *\n\
|
||||
*********************************\n\
|
||||
This may be caused by a man-in-the-middle attack, or the \
|
||||
server may have changed its host key.\n\
|
||||
\n\
|
||||
The {key_type_short_name} fingerprint for the key from the remote host is:\n\
|
||||
SHA256:{remote_fingerprint}\n\
|
||||
\n\
|
||||
You are strongly encouraged to contact the server \
|
||||
administrator for `{hostname}` to verify that this new key is \
|
||||
correct.\n\
|
||||
\n\
|
||||
If you can verify that the server has a new key, you can \
|
||||
resolve this error by {old_key_resolution}\n\
|
||||
\n\
|
||||
The key provided by the remote host is:\n\
|
||||
\n\
|
||||
{hostname} {key_type_name} {remote_host_key}\n\
|
||||
\n\
|
||||
See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
|
||||
for more information.\n\
|
||||
")
|
||||
}
|
||||
};
|
||||
Err(git2::Error::new(
|
||||
git2::ErrorCode::GenericError,
|
||||
git2::ErrorClass::Callback,
|
||||
err_msg,
|
||||
))
|
||||
}
|
||||
|
||||
/// Checks if the given host/host key pair is known.
|
||||
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());
|
||||
};
|
||||
let remote_key_type = cert_host_key.hostkey_type().unwrap();
|
||||
|
||||
// Collect all the known host entries from disk.
|
||||
let mut known_hosts = Vec::new();
|
||||
for path in known_host_files() {
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
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.
|
||||
let configured_hosts: HashSet<_> = known_hosts
|
||||
.iter()
|
||||
.flat_map(|known_host| {
|
||||
known_host
|
||||
.patterns
|
||||
.split(',')
|
||||
.map(|pattern| pattern.to_lowercase())
|
||||
})
|
||||
.collect();
|
||||
for (patterns, key_type, key) in BUNDLED_KEYS {
|
||||
if !configured_hosts.contains(*patterns) {
|
||||
let key = base64::decode(key).unwrap();
|
||||
known_hosts.push(KnownHost {
|
||||
location: KnownHostLocation::Bundled,
|
||||
patterns: patterns.to_string(),
|
||||
key_type: key_type.to_string(),
|
||||
key,
|
||||
});
|
||||
}
|
||||
}
|
||||
check_ssh_known_hosts_loaded(&known_hosts, host, remote_key_type, remote_host_key)
|
||||
}
|
||||
|
||||
/// Checks a host key against a loaded set of known hosts.
|
||||
fn check_ssh_known_hosts_loaded(
|
||||
known_hosts: &[KnownHost],
|
||||
host: &str,
|
||||
remote_key_type: SshHostKeyType,
|
||||
remote_host_key: &[u8],
|
||||
) -> Result<(), KnownHostError> {
|
||||
// `changed_key` keeps track of any entries where the key has changed.
|
||||
let mut changed_key = None;
|
||||
// `other_hosts` keeps track of any entries that have an identical key,
|
||||
// but a different hostname.
|
||||
let mut other_hosts = Vec::new();
|
||||
|
||||
for known_host in known_hosts {
|
||||
// The key type from libgit2 needs to match the key type from the host file.
|
||||
if known_host.key_type != remote_key_type.name() {
|
||||
continue;
|
||||
}
|
||||
let key_matches = known_host.key == remote_host_key;
|
||||
if !known_host.host_matches(host) {
|
||||
if key_matches {
|
||||
other_hosts.push(known_host.clone());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if key_matches {
|
||||
return Ok(());
|
||||
}
|
||||
// The host and key type matched, but the key itself did not.
|
||||
// This indicates the key has changed.
|
||||
// This is only reported as an error if no subsequent lines have a
|
||||
// correct key.
|
||||
changed_key = Some(known_host.clone());
|
||||
}
|
||||
// Older versions of OpenSSH (before 6.8, March 2015) showed MD5
|
||||
// fingerprints (see FingerprintHash ssh config option). Here we only
|
||||
// support SHA256.
|
||||
let mut remote_fingerprint = cargo_util::Sha256::new();
|
||||
remote_fingerprint.update(remote_host_key);
|
||||
let remote_fingerprint =
|
||||
base64::encode_config(remote_fingerprint.finish(), base64::STANDARD_NO_PAD);
|
||||
let remote_host_key = base64::encode(remote_host_key);
|
||||
// FIXME: Ideally the error message should include the IP address of the
|
||||
// remote host (to help the user validate that they are connecting to the
|
||||
// host they were expecting to). However, I don't see a way to obtain that
|
||||
// information from libgit2.
|
||||
match changed_key {
|
||||
Some(old_known_host) => Err(KnownHostError::HostKeyHasChanged {
|
||||
hostname: host.to_string(),
|
||||
key_type: remote_key_type,
|
||||
old_known_host,
|
||||
remote_host_key,
|
||||
remote_fingerprint,
|
||||
}),
|
||||
None => Err(KnownHostError::HostKeyNotFound {
|
||||
hostname: host.to_string(),
|
||||
key_type: remote_key_type,
|
||||
remote_host_key,
|
||||
remote_fingerprint,
|
||||
other_hosts,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of files to try loading OpenSSH-formatted known hosts.
|
||||
fn known_host_files() -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
if cfg!(unix) {
|
||||
result.push(PathBuf::from("/etc/ssh/ssh_known_hosts"));
|
||||
} else if cfg!(windows) {
|
||||
// The msys/cygwin version of OpenSSH uses `/etc` from the posix root
|
||||
// filesystem there (such as `C:\msys64\etc\ssh\ssh_known_hosts`).
|
||||
// However, I do not know of a way to obtain that location from
|
||||
// Windows-land. The ProgramData version here is what the PowerShell
|
||||
// port of OpenSSH does.
|
||||
if let Some(progdata) = std::env::var_os("ProgramData") {
|
||||
let mut progdata = PathBuf::from(progdata);
|
||||
progdata.push("ssh");
|
||||
progdata.push("ssh_known_hosts");
|
||||
result.push(progdata)
|
||||
}
|
||||
}
|
||||
result.extend(user_known_host_location());
|
||||
result
|
||||
}
|
||||
|
||||
/// The location of the user's known_hosts file.
|
||||
fn user_known_host_location() -> Option<PathBuf> {
|
||||
// NOTE: This is a potentially inaccurate prediction of what the user
|
||||
// actually wants. The actual location depends on several factors:
|
||||
//
|
||||
// - Windows OpenSSH Powershell version: I believe this looks up the home
|
||||
// directory via ProfileImagePath in the registry, falling back to
|
||||
// `GetWindowsDirectoryW` if that fails.
|
||||
// - OpenSSH Portable (under msys): This is very complicated. I got lost
|
||||
// after following it through some ldap/active directory stuff.
|
||||
// - OpenSSH (most unix platforms): Uses `pw->pw_dir` from `getpwuid()`.
|
||||
//
|
||||
// This doesn't do anything close to that. home_dir's behavior is:
|
||||
// - Windows: $USERPROFILE, or SHGetFolderPathW()
|
||||
// - Unix: $HOME, or getpwuid_r()
|
||||
//
|
||||
// Since there is a mismatch here, the location returned here might be
|
||||
// different than what the user's `ssh` CLI command uses. We may want to
|
||||
// consider trying to align it better.
|
||||
home::home_dir().map(|mut home| {
|
||||
home.push(".ssh");
|
||||
home.push("known_hosts");
|
||||
home
|
||||
})
|
||||
}
|
||||
|
||||
/// The location to display in an error message instructing the user where to
|
||||
/// add the new key.
|
||||
fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String {
|
||||
// Note that we don't bother with the legacy known_hosts2 files.
|
||||
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}"
|
||||
)
|
||||
}
|
||||
|
||||
const HASH_HOSTNAME_PREFIX: &str = "|1|";
|
||||
|
||||
/// A single known host entry.
|
||||
#[derive(Clone)]
|
||||
struct KnownHost {
|
||||
location: KnownHostLocation,
|
||||
/// The hostname. May be comma separated to match multiple hosts.
|
||||
patterns: String,
|
||||
key_type: String,
|
||||
key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl KnownHost {
|
||||
/// Returns whether or not the given host matches this known host entry.
|
||||
fn host_matches(&self, host: &str) -> bool {
|
||||
let mut match_found = false;
|
||||
let host = host.to_lowercase();
|
||||
if let Some(hashed) = self.patterns.strip_prefix(HASH_HOSTNAME_PREFIX) {
|
||||
return hashed_hostname_matches(&host, hashed);
|
||||
}
|
||||
for pattern in self.patterns.split(',') {
|
||||
let pattern = pattern.to_lowercase();
|
||||
// FIXME: support * and ? wildcards
|
||||
if let Some(pattern) = pattern.strip_prefix('!') {
|
||||
if pattern == host {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
match_found |= pattern == host;
|
||||
}
|
||||
}
|
||||
match_found
|
||||
}
|
||||
}
|
||||
|
||||
fn hashed_hostname_matches(host: &str, hashed: &str) -> bool {
|
||||
let Some((b64_salt, b64_host)) = hashed.split_once('|') else { return false; };
|
||||
let Ok(salt) = base64::decode(b64_salt) else { return false; };
|
||||
let Ok(hashed_host) = base64::decode(b64_host) else { return false; };
|
||||
let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt) else { return false; };
|
||||
mac.update(host.as_bytes());
|
||||
let result = mac.finalize().into_bytes();
|
||||
hashed_host == &result[..]
|
||||
}
|
||||
|
||||
/// Loads an OpenSSH known_hosts file.
|
||||
fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> {
|
||||
let contents = cargo_util::paths::read(path)?;
|
||||
Ok(load_hostfile_contents(path, &contents))
|
||||
}
|
||||
|
||||
fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> {
|
||||
let entries = contents
|
||||
.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(lineno, line)| {
|
||||
let location = KnownHostLocation::File {
|
||||
path: path.to_path_buf(),
|
||||
lineno: lineno as u32 + 1,
|
||||
};
|
||||
parse_known_hosts_line(line, location)
|
||||
})
|
||||
.collect();
|
||||
entries
|
||||
}
|
||||
|
||||
fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> {
|
||||
let line = line.trim();
|
||||
// FIXME: @revoked and @cert-authority is currently not supported.
|
||||
if line.is_empty() || line.starts_with(['#', '@']) {
|
||||
return None;
|
||||
}
|
||||
let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty());
|
||||
let patterns = parts.next()?;
|
||||
let key_type = parts.next()?;
|
||||
let key = parts.next().map(base64::decode)?.ok()?;
|
||||
Some(KnownHost {
|
||||
location,
|
||||
patterns: patterns.to_string(),
|
||||
key_type: key_type.to_string(),
|
||||
key,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static COMMON_CONTENTS: &str = r#"
|
||||
# Comments allowed at start of line
|
||||
|
||||
example.com,rust-lang.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5MzWIpZwpkpDjyCNiTIEVFhSA9OUUQvjFo7CgZBGCAj/cqeUIgiLsgtfmtBsfWIkAECQpM7ePP7NLZFGJcHvoyg5jXJiIX5s0eKo9IlcuTLLrMkW5MkHXE7bNklVbW1WdCfF2+y7Ao25B4L8FFRokMh0yp/H6+8xZ7PdVwL3FRPEg8ftZ5R0kuups6xiMHPRX+f/07vfJzA47YDPmXfhkn+JK8kL0JYw8iy8BtNBfRQL99d9iXJzWXnNce5NHMuKD5rOonD3aQHLDlwK+KhrFRrdaxQEM8ZWxNti0ux8yT4Dl5jJY0CrIu3Xl6+qroVgTqJGNkTbhs5DGWdFh6BLPTTH15rN4buisg7uMyLyHqx06ckborqD33gWu+Jig7O+PV6KJmL5mp1O1HXvZqkpBdTiT6GiDKG3oECCIXkUk0BSU9VG9VQcrMxxvgiHlyoXUAfYQoXv/lnxkTnm+Sr36kutsVOs7n5B43ZKAeuaxyQ11huJZpxamc0RA1HM641s= eric@host
|
||||
Example.net ssh-dss AAAAB3NzaC1kc3MAAACBAK2Ek3jVxisXmz5UcZ7W65BAj/nDJCCVvSe0Aytndn4PH6k7sVesut5OoY6PdksZ9tEfuFjjS9HR5SJb8j1GW0GxtaSHHbf+rNc36PeU75bffzyIWwpA8uZFONt5swUAXJXcsHOoapNbUFuhHsRhB2hXxz9QGNiiwIwRJeSHixKRAAAAFQChKfxO1z9H2/757697xP5nJ/Z5dwAAAIEAoc+HIWas+4WowtB/KtAp6XE0B9oHI+55wKtdcGwwb7zHKK9scWNXwxIcMhSvyB3Oe2I7dQQlvyIWxsdZlzOkX0wdsTHjIAnBAP68MyvMv4kq3+I5GAVcFsqoLZfZvh0dlcgUq1/YNYZwKlt89tnzk8Fp4KLWmuw8Bd8IShYVa78AAACAL3qd8kNTY7CthgsQ8iWdjbkGSF/1KCeFyt8UjurInp9wvPDjqagwakbyLOzN7y3/ItTPCaGuX+RjFP0zZTf8i9bsAVyjFJiJ7vzRXcWytuFWANrpzLTn1qzPfh63iK92Aw8AVBYvEA/4bxo+XReAvhNBB/m78G6OedTeu6ZoTsI= eric@host
|
||||
[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
|
||||
nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host
|
||||
nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host
|
||||
nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host
|
||||
# Revoked not yet supported.
|
||||
@revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host
|
||||
example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host
|
||||
192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host
|
||||
|1|QxzZoTXIWLhUsuHAXjuDMIV3FjQ=|M6NCOIkjiWdCWqkh5+Q+/uFLGjs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host
|
||||
# Negation isn't terribly useful without globs.
|
||||
neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn known_hosts_parse() {
|
||||
let kh_path = Path::new("/home/abc/.known_hosts");
|
||||
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
|
||||
assert_eq!(khs.len(), 10);
|
||||
match &khs[0].location {
|
||||
KnownHostLocation::File { path, lineno } => {
|
||||
assert_eq!(path, kh_path);
|
||||
assert_eq!(*lineno, 4);
|
||||
}
|
||||
_ => panic!("unexpected"),
|
||||
}
|
||||
assert_eq!(khs[0].patterns, "example.com,rust-lang.org");
|
||||
assert_eq!(khs[0].key_type, "ssh-rsa");
|
||||
assert_eq!(khs[0].key.len(), 407);
|
||||
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)");
|
||||
match &khs[1].location {
|
||||
KnownHostLocation::File { path, lineno } => {
|
||||
assert_eq!(path, kh_path);
|
||||
assert_eq!(*lineno, 5);
|
||||
}
|
||||
_ => panic!("unexpected"),
|
||||
}
|
||||
assert_eq!(khs[2].patterns, "[example.net]:2222");
|
||||
assert_eq!(khs[3].patterns, "nistp256.example.org");
|
||||
assert_eq!(khs[7].patterns, "192.168.42.12");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_matches() {
|
||||
let kh_path = Path::new("/home/abc/.known_hosts");
|
||||
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
|
||||
assert!(khs[0].host_matches("example.com"));
|
||||
assert!(khs[0].host_matches("rust-lang.org"));
|
||||
assert!(khs[0].host_matches("EXAMPLE.COM"));
|
||||
assert!(khs[1].host_matches("example.net"));
|
||||
assert!(!khs[0].host_matches("example.net"));
|
||||
assert!(khs[2].host_matches("[example.net]:2222"));
|
||||
assert!(!khs[2].host_matches("example.net"));
|
||||
assert!(khs[8].host_matches("hashed.example.com"));
|
||||
assert!(!khs[8].host_matches("example.com"));
|
||||
assert!(!khs[9].host_matches("neg.example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_match() {
|
||||
let kh_path = Path::new("/home/abc/.known_hosts");
|
||||
let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS);
|
||||
|
||||
assert!(check_ssh_known_hosts_loaded(
|
||||
&khs,
|
||||
"example.com",
|
||||
SshHostKeyType::Rsa,
|
||||
&khs[0].key
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Dss, &khs[0].key) {
|
||||
Err(KnownHostError::HostKeyNotFound {
|
||||
hostname,
|
||||
remote_fingerprint,
|
||||
other_hosts,
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(
|
||||
remote_fingerprint,
|
||||
"yn+pONDn0EcgdOCVptgB4RZd/wqmsVKrPnQMLtrvhw8"
|
||||
);
|
||||
assert_eq!(hostname, "example.com");
|
||||
assert_eq!(other_hosts.len(), 0);
|
||||
}
|
||||
_ => panic!("unexpected"),
|
||||
}
|
||||
|
||||
match check_ssh_known_hosts_loaded(
|
||||
&khs,
|
||||
"foo.example.com",
|
||||
SshHostKeyType::Rsa,
|
||||
&khs[0].key,
|
||||
) {
|
||||
Err(KnownHostError::HostKeyNotFound { other_hosts, .. }) => {
|
||||
assert_eq!(other_hosts.len(), 1);
|
||||
assert_eq!(other_hosts[0].patterns, "example.com,rust-lang.org");
|
||||
}
|
||||
_ => panic!("unexpected"),
|
||||
}
|
||||
|
||||
let mut modified_key = khs[0].key.clone();
|
||||
modified_key[0] = 1;
|
||||
match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Rsa, &modified_key)
|
||||
{
|
||||
Err(KnownHostError::HostKeyHasChanged { old_known_host, .. }) => {
|
||||
assert!(matches!(
|
||||
old_known_host.location,
|
||||
KnownHostLocation::File { lineno: 4, .. }
|
||||
));
|
||||
}
|
||||
_ => panic!("unexpected"),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
pub use self::source::GitSource;
|
||||
pub use self::utils::{fetch, GitCheckout, GitDatabase, GitRemote};
|
||||
mod known_hosts;
|
||||
mod source;
|
||||
mod utils;
|
||||
|
@ -683,7 +683,6 @@ where
|
||||
| ErrorClass::Submodule
|
||||
| ErrorClass::FetchHead
|
||||
| ErrorClass::Ssh
|
||||
| ErrorClass::Callback
|
||||
| ErrorClass::Http => {
|
||||
let mut msg = "network failure seems to have happened\n".to_string();
|
||||
msg.push_str(
|
||||
@ -694,6 +693,13 @@ where
|
||||
);
|
||||
err = err.context(msg);
|
||||
}
|
||||
ErrorClass::Callback => {
|
||||
// This unwraps the git2 error. We're using the callback error
|
||||
// specifically to convey errors from Rust land through the C
|
||||
// callback interface. We don't need the `; class=Callback
|
||||
// (26)` that gets tacked on to the git2 error message.
|
||||
err = anyhow::format_err!("{}", e.message());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@ -720,14 +726,27 @@ 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());
|
||||
let mut last_update = Instant::now();
|
||||
let mut rcb = git2::RemoteCallbacks::new();
|
||||
// We choose `N=10` here to make a `300ms * 10slots ~= 3000ms`
|
||||
// sliding window for tracking the data transfer rate (in bytes/s).
|
||||
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,
|
||||
config_known_hosts,
|
||||
&diagnostic_home_config,
|
||||
)
|
||||
});
|
||||
rcb.transfer_progress(|stats| {
|
||||
let indexed_deltas = stats.indexed_deltas();
|
||||
let msg = if indexed_deltas > 0 {
|
||||
|
@ -384,7 +384,12 @@ impl<'de> de::SeqAccess<'de> for ConfigSeqAccess {
|
||||
{
|
||||
match self.list_iter.next() {
|
||||
// TODO: add `def` to error?
|
||||
Some((value, _def)) => seed.deserialize(value.into_deserializer()).map(Some),
|
||||
Some((value, def)) => {
|
||||
// This might be a String or a Value<String>.
|
||||
// ValueDeserializer will handle figuring out which one it is.
|
||||
let maybe_value_de = ValueDeserializer::new_with_string(value, def);
|
||||
seed.deserialize(maybe_value_de).map(Some)
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@ -400,7 +405,17 @@ impl<'de> de::SeqAccess<'de> for ConfigSeqAccess {
|
||||
struct ValueDeserializer<'config> {
|
||||
hits: u32,
|
||||
definition: Definition,
|
||||
de: Deserializer<'config>,
|
||||
/// The deserializer, used to actually deserialize a Value struct.
|
||||
/// This is `None` if deserializing a string.
|
||||
de: Option<Deserializer<'config>>,
|
||||
/// A string value to deserialize.
|
||||
///
|
||||
/// This is used for situations where you can't address a string via a
|
||||
/// TOML key, such as a string inside an array. The `ConfigSeqAccess`
|
||||
/// doesn't know if the type it should deserialize to is a `String` or
|
||||
/// `Value<String>`, so `ValueDeserializer` needs to be able to handle
|
||||
/// both.
|
||||
str_value: Option<String>,
|
||||
}
|
||||
|
||||
impl<'config> ValueDeserializer<'config> {
|
||||
@ -428,9 +443,19 @@ impl<'config> ValueDeserializer<'config> {
|
||||
Ok(ValueDeserializer {
|
||||
hits: 0,
|
||||
definition,
|
||||
de,
|
||||
de: Some(de),
|
||||
str_value: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn new_with_string(s: String, definition: Definition) -> ValueDeserializer<'config> {
|
||||
ValueDeserializer {
|
||||
hits: 0,
|
||||
definition,
|
||||
de: None,
|
||||
str_value: Some(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> {
|
||||
@ -459,9 +484,14 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> {
|
||||
// If this is the first time around we deserialize the `value` field
|
||||
// which is the actual deserializer
|
||||
if self.hits == 1 {
|
||||
return seed
|
||||
.deserialize(self.de.clone())
|
||||
.map_err(|e| e.with_key_context(&self.de.key, self.definition.clone()));
|
||||
if let Some(de) = &self.de {
|
||||
return seed
|
||||
.deserialize(de.clone())
|
||||
.map_err(|e| e.with_key_context(&de.key, self.definition.clone()));
|
||||
} else {
|
||||
return seed
|
||||
.deserialize(self.str_value.as_ref().unwrap().clone().into_deserializer());
|
||||
}
|
||||
}
|
||||
|
||||
// ... otherwise we're deserializing the `definition` field, so we need
|
||||
@ -484,6 +514,71 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> {
|
||||
}
|
||||
}
|
||||
|
||||
// Deserializer is only implemented to handle deserializing a String inside a
|
||||
// sequence (like `Vec<String>` or `Vec<Value<String>>`). `Value<String>` is
|
||||
// handled by deserialize_struct, and the plain `String` is handled by all the
|
||||
// other functions here.
|
||||
impl<'de, 'config> de::Deserializer<'de> for ValueDeserializer<'config> {
|
||||
type Error = ConfigError;
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
visitor.visit_str(&self.str_value.expect("string expected"))
|
||||
}
|
||||
|
||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
visitor.visit_string(self.str_value.expect("string expected"))
|
||||
}
|
||||
|
||||
fn deserialize_struct<V>(
|
||||
self,
|
||||
name: &'static str,
|
||||
fields: &'static [&'static str],
|
||||
visitor: V,
|
||||
) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
// Match on the magical struct name/field names that are passed in to
|
||||
// detect when we're deserializing `Value<T>`.
|
||||
//
|
||||
// See more comments in `value.rs` for the protocol used here.
|
||||
if name == value::NAME && fields == value::FIELDS {
|
||||
return visitor.visit_map(self);
|
||||
}
|
||||
unimplemented!("only strings and Value can be deserialized from a sequence");
|
||||
}
|
||||
|
||||
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
visitor.visit_string(self.str_value.expect("string expected"))
|
||||
}
|
||||
|
||||
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: de::Visitor<'de>,
|
||||
{
|
||||
visitor.visit_unit()
|
||||
}
|
||||
|
||||
serde::forward_to_deserialize_any! {
|
||||
i8 i16 i32 i64
|
||||
u8 u16 u32 u64
|
||||
option
|
||||
newtype_struct seq tuple tuple_struct map enum bool
|
||||
f32 f64 char bytes
|
||||
byte_buf unit unit_struct
|
||||
identifier
|
||||
}
|
||||
}
|
||||
|
||||
/// A deserializer which takes two values and deserializes into a tuple of those
|
||||
/// two values. This is similar to types like `StrDeserializer` in upstream
|
||||
/// serde itself.
|
||||
|
@ -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)]
|
||||
|
@ -58,9 +58,34 @@ on how to start `ssh-agent` and to add keys.
|
||||
> used by Cargo's built-in SSH library. More advanced requirements should use
|
||||
> [`net.git-fetch-with-cli`].
|
||||
|
||||
### SSH Known Hosts
|
||||
|
||||
When connecting to an SSH host, Cargo must verify the identity of the host
|
||||
using "known hosts", which are a list of host keys. Cargo can look for these
|
||||
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]. 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.
|
||||
This also includes a "fingerprint", which is a smaller hash of the host key,
|
||||
which should be easier to visually verify. The server administrator can get
|
||||
the fingerprint by running `ssh-keygen` against the public key (for example,
|
||||
`ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub`). Well-known sites may
|
||||
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 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
|
||||
[key management]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement
|
||||
[sshd man page]: https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,7 @@
|
||||
//! Tests for config settings.
|
||||
|
||||
use cargo::core::{PackageIdSpec, Shell};
|
||||
use cargo::util::config::{self, Config, SslVersionConfig, StringList};
|
||||
use cargo::util::config::{self, Config, Definition, SslVersionConfig, StringList};
|
||||
use cargo::util::interning::InternedString;
|
||||
use cargo::util::toml::{self, VecStringOrBool as VSOB};
|
||||
use cargo::CargoResult;
|
||||
@ -1508,3 +1508,59 @@ fn all_profile_options() {
|
||||
let roundtrip_toml = toml_edit::easy::to_string(&roundtrip).unwrap();
|
||||
compare::assert_match_exact(&profile_toml, &roundtrip_toml);
|
||||
}
|
||||
|
||||
#[cargo_test]
|
||||
fn value_in_array() {
|
||||
// Value<String> in an array should work
|
||||
let root_path = paths::root().join(".cargo/config.toml");
|
||||
write_config_at(
|
||||
&root_path,
|
||||
"\
|
||||
[net.ssh]
|
||||
known-hosts = [
|
||||
\"example.com ...\",
|
||||
\"example.net ...\",
|
||||
]
|
||||
",
|
||||
);
|
||||
|
||||
let foo_path = paths::root().join("foo/.cargo/config.toml");
|
||||
write_config_at(
|
||||
&foo_path,
|
||||
"\
|
||||
[net.ssh]
|
||||
known-hosts = [
|
||||
\"example.org ...\",
|
||||
]
|
||||
",
|
||||
);
|
||||
|
||||
let config = ConfigBuilder::new()
|
||||
.cwd("foo")
|
||||
// environment variables don't actually work for known-hosts due to
|
||||
// space splitting, but this is included here just to validate that
|
||||
// they work (particularly if other Vec<Value> config vars are added
|
||||
// in the future).
|
||||
.env("CARGO_NET_SSH_KNOWN_HOSTS", "env-example")
|
||||
.build();
|
||||
let net_config = config.net_config().unwrap();
|
||||
let kh = net_config
|
||||
.ssh
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.known_hosts
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
assert_eq!(kh.len(), 4);
|
||||
assert_eq!(kh[0].val, "example.org ...");
|
||||
assert_eq!(kh[0].definition, Definition::Path(foo_path.clone()));
|
||||
assert_eq!(kh[1].val, "example.com ...");
|
||||
assert_eq!(kh[1].definition, Definition::Path(root_path.clone()));
|
||||
assert_eq!(kh[2].val, "example.net ...");
|
||||
assert_eq!(kh[2].definition, Definition::Path(root_path.clone()));
|
||||
assert_eq!(kh[3].val, "env-example");
|
||||
assert_eq!(
|
||||
kh[3].definition,
|
||||
Definition::Environment("CARGO_NET_SSH_KNOWN_HOSTS".to_string())
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user