Cargo - CVE fix and git2 update.
Peter Michael Green
1 year, 3 months ago
0 | 0 | rust-cargo (0.66.0-1) UNRELEASED-FIXME-AUTOGENERATED-DEBCARGO; urgency=medium |
1 | 1 | |
2 | * Package cargo 0.66.0 from crates.io using debcargo 2.6.0 | |
3 | * Backport upstream patches to fix CVE-2022-46176 and update | |
4 | git2 dependencies. | |
5 | ||
6 | [ Fabian Gruenbichler ] | |
2 | 7 | * Team upload. |
3 | 8 | * Package cargo 0.66.0 from crates.io using debcargo 2.6.0 |
4 | 9 | |
5 | -- Fabian Gruenbichler <debian@fabian.gruenbichler.email> Sun, 08 Jan 2023 16:42:17 +0100 | |
10 | -- Peter Michael Green <plugwash@debian.org> Wed, 11 Jan 2023 03:06:58 +0000 | |
6 | 11 | |
7 | 12 | rust-cargo (0.63.1-2) unstable; urgency=medium |
8 | 13 |
0 | This patch is based on the upstream commit described below, adapted for use | |
1 | in the Debian package by Peter Michael Green. | |
2 | ||
3 | commit 1387fd4105b242fa2d24ad99d10a5b1af23f293e | |
4 | Author: Eric Huss <eric@huss.org> | |
5 | Date: Wed Dec 7 18:52:00 2022 -0800 | |
6 | ||
7 | Validate SSH host keys | |
8 | ||
9 | Index: cargo/src/cargo/sources/git/known_hosts.rs | |
10 | =================================================================== | |
11 | --- /dev/null | |
12 | +++ cargo/src/cargo/sources/git/known_hosts.rs | |
13 | @@ -0,0 +1,439 @@ | |
14 | +//! SSH host key validation support. | |
15 | +//! | |
16 | +//! A primary goal with this implementation is to provide user-friendly error | |
17 | +//! messages, guiding them to understand the issue and how to resolve it. | |
18 | +//! | |
19 | +//! Note that there are a lot of limitations here. This reads OpenSSH | |
20 | +//! known_hosts files from well-known locations, but it does not read OpenSSH | |
21 | +//! config files. The config file can change the behavior of how OpenSSH | |
22 | +//! handles known_hosts files. For example, some things we don't handle: | |
23 | +//! | |
24 | +//! - `GlobalKnownHostsFile` — Changes the location of the global host file. | |
25 | +//! - `UserKnownHostsFile` — Changes the location of the user's host file. | |
26 | +//! - `KnownHostsCommand` — A command to fetch known hosts. | |
27 | +//! - `CheckHostIP` — DNS spoofing checks. | |
28 | +//! - `VisualHostKey` — Shows a visual ascii-art key. | |
29 | +//! - `VerifyHostKeyDNS` — Uses SSHFP DNS records to fetch a host key. | |
30 | +//! | |
31 | +//! There's also a number of things that aren't supported but could be easily | |
32 | +//! added (it just adds a little complexity). For example, hashed hostnames, | |
33 | +//! hostname patterns, and revoked markers. See "FIXME" comments littered in | |
34 | +//! this file. | |
35 | + | |
36 | +use git2::cert::Cert; | |
37 | +use git2::CertificateCheckStatus; | |
38 | +use std::collections::HashSet; | |
39 | +use std::fmt::Write; | |
40 | +use std::path::{Path, PathBuf}; | |
41 | + | |
42 | +/// These are host keys that are hard-coded in cargo to provide convenience. | |
43 | +/// | |
44 | +/// If GitHub ever publishes new keys, the user can add them to their own | |
45 | +/// configuration file to use those instead. | |
46 | +/// | |
47 | +/// The GitHub keys are sourced from <https://api.github.com/meta> or | |
48 | +/// <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>. | |
49 | +/// | |
50 | +/// These will be ignored if the user adds their own entries for `github.com`, | |
51 | +/// which can be useful if GitHub ever revokes their old keys. | |
52 | +static BUNDLED_KEYS: &[(&str, &str, &str)] = &[ | |
53 | + ("github.com", "ssh-ed25519", "AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"), | |
54 | + ("github.com", "ecdsa-sha2-nistp256", "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg="), | |
55 | + ("github.com", "ssh-rsa", "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ=="), | |
56 | +]; | |
57 | + | |
58 | +enum KnownHostError { | |
59 | + /// Some general error happened while validating the known hosts. | |
60 | + CheckError(anyhow::Error), | |
61 | + /// The host key was not found. | |
62 | + HostKeyNotFound { | |
63 | + hostname: String, | |
64 | + key_type: git2::cert::SshHostKeyType, | |
65 | + remote_host_key: String, | |
66 | + remote_fingerprint: String, | |
67 | + other_hosts: Vec<KnownHost>, | |
68 | + }, | |
69 | + /// The host key was found, but does not match the remote's key. | |
70 | + HostKeyHasChanged { | |
71 | + hostname: String, | |
72 | + key_type: git2::cert::SshHostKeyType, | |
73 | + old_known_host: KnownHost, | |
74 | + remote_host_key: String, | |
75 | + remote_fingerprint: String, | |
76 | + }, | |
77 | +} | |
78 | + | |
79 | +impl From<anyhow::Error> for KnownHostError { | |
80 | + fn from(err: anyhow::Error) -> KnownHostError { | |
81 | + KnownHostError::CheckError(err.into()) | |
82 | + } | |
83 | +} | |
84 | + | |
85 | +/// The location where a host key was located. | |
86 | +#[derive(Clone)] | |
87 | +enum KnownHostLocation { | |
88 | + /// Loaded from a file from disk. | |
89 | + File { path: PathBuf, lineno: u32 }, | |
90 | + /// Part of the hard-coded bundled keys in Cargo. | |
91 | + Bundled, | |
92 | +} | |
93 | + | |
94 | +/// The git2 callback used to validate a certificate (only ssh known hosts are validated). | |
95 | +pub fn certificate_check( | |
96 | + cert: &Cert<'_>, | |
97 | + host: &str, | |
98 | + port: Option<u16>, | |
99 | +) -> Result<CertificateCheckStatus, git2::Error> { | |
100 | + let Some(host_key) = cert.as_hostkey() else { | |
101 | + // Return passthrough for TLS X509 certificates to use whatever validation | |
102 | + // was done in git2. | |
103 | + return Ok(CertificateCheckStatus::CertificatePassthrough) | |
104 | + }; | |
105 | + // If a nonstandard port is in use, check for that first. | |
106 | + // The fallback to check without a port is handled in the HostKeyNotFound handler. | |
107 | + let host_maybe_port = match port { | |
108 | + Some(port) if port != 22 => format!("[{host}]:{port}"), | |
109 | + _ => host.to_string(), | |
110 | + }; | |
111 | + // The error message must be constructed as a string to pass through the libgit2 C API. | |
112 | + let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port) { | |
113 | + Ok(()) => { | |
114 | + return Ok(CertificateCheckStatus::CertificateOk); | |
115 | + } | |
116 | + Err(KnownHostError::CheckError(e)) => { | |
117 | + format!("error: failed to validate host key:\n{:#}", e) | |
118 | + } | |
119 | + Err(KnownHostError::HostKeyNotFound { | |
120 | + hostname, | |
121 | + key_type, | |
122 | + remote_host_key, | |
123 | + remote_fingerprint, | |
124 | + other_hosts, | |
125 | + }) => { | |
126 | + // Try checking without the port. | |
127 | + if port.is_some() | |
128 | + && !matches!(port, Some(22)) | |
129 | + && check_ssh_known_hosts(host_key, host).is_ok() | |
130 | + { | |
131 | + return Ok(CertificateCheckStatus::CertificateOk); | |
132 | + } | |
133 | + let key_type_short_name = key_type.short_name(); | |
134 | + let key_type_name = key_type.name(); | |
135 | + let known_hosts_location = user_known_host_location_to_add(); | |
136 | + let other_hosts_message = if other_hosts.is_empty() { | |
137 | + String::new() | |
138 | + } else { | |
139 | + let mut msg = String::from( | |
140 | + "Note: This host key was found, \ | |
141 | + but is associated with a different host:\n", | |
142 | + ); | |
143 | + for known_host in other_hosts { | |
144 | + let loc = match known_host.location { | |
145 | + KnownHostLocation::File { path, lineno } => { | |
146 | + format!("{} line {lineno}", path.display()) | |
147 | + } | |
148 | + KnownHostLocation::Bundled => format!("bundled with cargo"), | |
149 | + }; | |
150 | + write!(msg, " {loc}: {}\n", known_host.patterns).unwrap(); | |
151 | + } | |
152 | + msg | |
153 | + }; | |
154 | + format!("error: unknown SSH host key\n\ | |
155 | + The SSH host key for `{hostname}` is not known and cannot be validated.\n\ | |
156 | + \n\ | |
157 | + To resolve this issue, add the host key to {known_hosts_location}\n\ | |
158 | + \n\ | |
159 | + The key to add is:\n\ | |
160 | + \n\ | |
161 | + {hostname} {key_type_name} {remote_host_key}\n\ | |
162 | + \n\ | |
163 | + The {key_type_short_name} key fingerprint is: SHA256:{remote_fingerprint}\n\ | |
164 | + This fingerprint should be validated with the server administrator that it is correct.\n\ | |
165 | + {other_hosts_message}\n\ | |
166 | + See https://doc.rust-lang.org/nightly/cargo/appendix/git-authentication.html#ssh-known-hosts \ | |
167 | + for more information.\n\ | |
168 | + ") | |
169 | + } | |
170 | + Err(KnownHostError::HostKeyHasChanged { | |
171 | + hostname, | |
172 | + key_type, | |
173 | + old_known_host, | |
174 | + remote_host_key, | |
175 | + remote_fingerprint, | |
176 | + }) => { | |
177 | + let key_type_short_name = key_type.short_name(); | |
178 | + let key_type_name = key_type.name(); | |
179 | + let known_hosts_location = user_known_host_location_to_add(); | |
180 | + let old_key_resolution = match old_known_host.location { | |
181 | + KnownHostLocation::File { path, lineno } => { | |
182 | + let old_key_location = path.display(); | |
183 | + format!( | |
184 | + "removing the old {key_type_name} key for `{hostname}` \ | |
185 | + located at {old_key_location} line {lineno}, \ | |
186 | + and adding the new key to {known_hosts_location}", | |
187 | + ) | |
188 | + } | |
189 | + KnownHostLocation::Bundled => { | |
190 | + format!( | |
191 | + "adding the new key to {known_hosts_location}\n\ | |
192 | + The current host key is bundled as part of Cargo." | |
193 | + ) | |
194 | + } | |
195 | + }; | |
196 | + format!("error: SSH host key has changed for `{hostname}`\n\ | |
197 | + *********************************\n\ | |
198 | + * WARNING: HOST KEY HAS CHANGED *\n\ | |
199 | + *********************************\n\ | |
200 | + This may be caused by a man-in-the-middle attack, or the \ | |
201 | + server may have changed its host key.\n\ | |
202 | + \n\ | |
203 | + The {key_type_short_name} fingerprint for the key from the remote host is:\n\ | |
204 | + SHA256:{remote_fingerprint}\n\ | |
205 | + \n\ | |
206 | + You are strongly encouraged to contact the server \ | |
207 | + administrator for `{hostname}` to verify that this new key is \ | |
208 | + correct.\n\ | |
209 | + \n\ | |
210 | + If you can verify that the server has a new key, you can \ | |
211 | + resolve this error by {old_key_resolution}\n\ | |
212 | + \n\ | |
213 | + The key provided by the remote host is:\n\ | |
214 | + \n\ | |
215 | + {hostname} {key_type_name} {remote_host_key}\n\ | |
216 | + \n\ | |
217 | + See https://doc.rust-lang.org/nightly/cargo/appendix/git-authentication.html#ssh-known-hosts \ | |
218 | + for more information.\n\ | |
219 | + ") | |
220 | + } | |
221 | + }; | |
222 | + Err(git2::Error::new( | |
223 | + git2::ErrorCode::GenericError, | |
224 | + git2::ErrorClass::Callback, | |
225 | + err_msg, | |
226 | + )) | |
227 | +} | |
228 | + | |
229 | +/// Checks if the given host/host key pair is known. | |
230 | +fn check_ssh_known_hosts( | |
231 | + cert_host_key: &git2::cert::CertHostkey<'_>, | |
232 | + host: &str, | |
233 | +) -> Result<(), KnownHostError> { | |
234 | + let Some(remote_host_key) = cert_host_key.hostkey() else { | |
235 | + return Err(anyhow::format_err!("remote host key is not available").into()); | |
236 | + }; | |
237 | + let remote_key_type = cert_host_key.hostkey_type().unwrap(); | |
238 | + // `changed_key` keeps track of any entries where the key has changed. | |
239 | + let mut changed_key = None; | |
240 | + // `other_hosts` keeps track of any entries that have an identical key, | |
241 | + // but a different hostname. | |
242 | + let mut other_hosts = Vec::new(); | |
243 | + | |
244 | + // Collect all the known host entries from disk. | |
245 | + let mut known_hosts = Vec::new(); | |
246 | + for path in known_host_files() { | |
247 | + if !path.exists() { | |
248 | + continue; | |
249 | + } | |
250 | + let hosts = load_hostfile(&path)?; | |
251 | + known_hosts.extend(hosts); | |
252 | + } | |
253 | + // Load the bundled keys. Don't add keys for hosts that the user has | |
254 | + // configured, which gives them the option to override them. This could be | |
255 | + // useful if the keys are ever revoked. | |
256 | + let configured_hosts: HashSet<_> = known_hosts | |
257 | + .iter() | |
258 | + .flat_map(|known_host| { | |
259 | + known_host | |
260 | + .patterns | |
261 | + .split(',') | |
262 | + .map(|pattern| pattern.to_lowercase()) | |
263 | + }) | |
264 | + .collect(); | |
265 | + for (patterns, key_type, key) in BUNDLED_KEYS { | |
266 | + if !configured_hosts.contains(*patterns) { | |
267 | + let key = base64::decode(key).unwrap(); | |
268 | + known_hosts.push(KnownHost { | |
269 | + location: KnownHostLocation::Bundled, | |
270 | + patterns: patterns.to_string(), | |
271 | + key_type: key_type.to_string(), | |
272 | + key, | |
273 | + }); | |
274 | + } | |
275 | + } | |
276 | + | |
277 | + for known_host in known_hosts { | |
278 | + // The key type from libgit2 needs to match the key type from the host file. | |
279 | + if known_host.key_type != remote_key_type.name() { | |
280 | + continue; | |
281 | + } | |
282 | + let key_matches = known_host.key == remote_host_key; | |
283 | + if !known_host.host_matches(host) { | |
284 | + // `name` can be None for hashed hostnames (which libgit2 does not expose). | |
285 | + if key_matches { | |
286 | + other_hosts.push(known_host.clone()); | |
287 | + } | |
288 | + continue; | |
289 | + } | |
290 | + if key_matches { | |
291 | + return Ok(()); | |
292 | + } | |
293 | + // The host and key type matched, but the key itself did not. | |
294 | + // This indicates the key has changed. | |
295 | + // This is only reported as an error if no subsequent lines have a | |
296 | + // correct key. | |
297 | + changed_key = Some(known_host.clone()); | |
298 | + } | |
299 | + // Older versions of OpenSSH (before 6.8, March 2015) showed MD5 | |
300 | + // fingerprints (see FingerprintHash ssh config option). Here we only | |
301 | + // support SHA256. | |
302 | + let mut remote_fingerprint = cargo_util::Sha256::new(); | |
303 | + remote_fingerprint.update(remote_host_key); | |
304 | + let remote_fingerprint = | |
305 | + base64::encode_config(remote_fingerprint.finish(), base64::STANDARD_NO_PAD); | |
306 | + let remote_host_key = base64::encode(remote_host_key); | |
307 | + // FIXME: Ideally the error message should include the IP address of the | |
308 | + // remote host (to help the user validate that they are connecting to the | |
309 | + // host they were expecting to). However, I don't see a way to obtain that | |
310 | + // information from libgit2. | |
311 | + match changed_key { | |
312 | + Some(old_known_host) => Err(KnownHostError::HostKeyHasChanged { | |
313 | + hostname: host.to_string(), | |
314 | + key_type: remote_key_type, | |
315 | + old_known_host, | |
316 | + remote_host_key, | |
317 | + remote_fingerprint, | |
318 | + }), | |
319 | + None => Err(KnownHostError::HostKeyNotFound { | |
320 | + hostname: host.to_string(), | |
321 | + key_type: remote_key_type, | |
322 | + remote_host_key, | |
323 | + remote_fingerprint, | |
324 | + other_hosts, | |
325 | + }), | |
326 | + } | |
327 | +} | |
328 | + | |
329 | +/// Returns a list of files to try loading OpenSSH-formatted known hosts. | |
330 | +fn known_host_files() -> Vec<PathBuf> { | |
331 | + let mut result = Vec::new(); | |
332 | + if cfg!(unix) { | |
333 | + result.push(PathBuf::from("/etc/ssh/ssh_known_hosts")); | |
334 | + } else if cfg!(windows) { | |
335 | + // The msys/cygwin version of OpenSSH uses `/etc` from the posix root | |
336 | + // filesystem there (such as `C:\msys64\etc\ssh\ssh_known_hosts`). | |
337 | + // However, I do not know of a way to obtain that location from | |
338 | + // Windows-land. The ProgramData version here is what the PowerShell | |
339 | + // port of OpenSSH does. | |
340 | + if let Some(progdata) = std::env::var_os("ProgramData") { | |
341 | + let mut progdata = PathBuf::from(progdata); | |
342 | + progdata.push("ssh"); | |
343 | + progdata.push("ssh_known_hosts"); | |
344 | + result.push(progdata) | |
345 | + } | |
346 | + } | |
347 | + result.extend(user_known_host_location()); | |
348 | + result | |
349 | +} | |
350 | + | |
351 | +/// The location of the user's known_hosts file. | |
352 | +fn user_known_host_location() -> Option<PathBuf> { | |
353 | + // NOTE: This is a potentially inaccurate prediction of what the user | |
354 | + // actually wants. The actual location depends on several factors: | |
355 | + // | |
356 | + // - Windows OpenSSH Powershell version: I believe this looks up the home | |
357 | + // directory via ProfileImagePath in the registry, falling back to | |
358 | + // `GetWindowsDirectoryW` if that fails. | |
359 | + // - OpenSSH Portable (under msys): This is very complicated. I got lost | |
360 | + // after following it through some ldap/active directory stuff. | |
361 | + // - OpenSSH (most unix platforms): Uses `pw->pw_dir` from `getpwuid()`. | |
362 | + // | |
363 | + // This doesn't do anything close to that. home_dir's behavior is: | |
364 | + // - Windows: $USERPROFILE, or SHGetFolderPathW() | |
365 | + // - Unix: $HOME, or getpwuid_r() | |
366 | + // | |
367 | + // Since there is a mismatch here, the location returned here might be | |
368 | + // different than what the user's `ssh` CLI command uses. We may want to | |
369 | + // consider trying to align it better. | |
370 | + home::home_dir().map(|mut home| { | |
371 | + home.push(".ssh"); | |
372 | + home.push("known_hosts"); | |
373 | + home | |
374 | + }) | |
375 | +} | |
376 | + | |
377 | +/// The location to display in an error message instructing the user where to | |
378 | +/// add the new key. | |
379 | +fn user_known_host_location_to_add() -> String { | |
380 | + // Note that we don't bother with the legacy known_hosts2 files. | |
381 | + match user_known_host_location() { | |
382 | + Some(path) => path.to_str().expect("utf-8 home").to_string(), | |
383 | + None => "~/.ssh/known_hosts".to_string(), | |
384 | + } | |
385 | +} | |
386 | + | |
387 | +/// A single known host entry. | |
388 | +#[derive(Clone)] | |
389 | +struct KnownHost { | |
390 | + location: KnownHostLocation, | |
391 | + /// The hostname. May be comma separated to match multiple hosts. | |
392 | + patterns: String, | |
393 | + key_type: String, | |
394 | + key: Vec<u8>, | |
395 | +} | |
396 | + | |
397 | +impl KnownHost { | |
398 | + /// Returns whether or not the given host matches this known host entry. | |
399 | + fn host_matches(&self, host: &str) -> bool { | |
400 | + let mut match_found = false; | |
401 | + let host = host.to_lowercase(); | |
402 | + // FIXME: support hashed hostnames | |
403 | + for pattern in self.patterns.split(',') { | |
404 | + let pattern = pattern.to_lowercase(); | |
405 | + // FIXME: support * and ? wildcards | |
406 | + if let Some(pattern) = pattern.strip_prefix('!') { | |
407 | + if pattern == host { | |
408 | + return false; | |
409 | + } | |
410 | + } else { | |
411 | + match_found = pattern == host; | |
412 | + } | |
413 | + } | |
414 | + match_found | |
415 | + } | |
416 | +} | |
417 | + | |
418 | +/// Loads an OpenSSH known_hosts file. | |
419 | +fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> { | |
420 | + let contents = cargo_util::paths::read(path)?; | |
421 | + let entries = contents | |
422 | + .lines() | |
423 | + .enumerate() | |
424 | + .filter_map(|(lineno, line)| { | |
425 | + let location = KnownHostLocation::File { | |
426 | + path: path.to_path_buf(), | |
427 | + lineno: lineno as u32 + 1, | |
428 | + }; | |
429 | + parse_known_hosts_line(line, location) | |
430 | + }) | |
431 | + .collect(); | |
432 | + Ok(entries) | |
433 | +} | |
434 | + | |
435 | +fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> { | |
436 | + let line = line.trim(); | |
437 | + // FIXME: @revoked and @cert-authority is currently not supported. | |
438 | + if line.is_empty() || line.starts_with('#') || line.starts_with('@') { | |
439 | + return None; | |
440 | + } | |
441 | + let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty()); | |
442 | + let Some(patterns) = parts.next() else { return None }; | |
443 | + let Some(key_type) = parts.next() else { return None }; | |
444 | + let Some(key) = parts.next() else { return None }; | |
445 | + let Ok(key) = base64::decode(key) else { return None }; | |
446 | + Some(KnownHost { | |
447 | + location, | |
448 | + patterns: patterns.to_string(), | |
449 | + key_type: key_type.to_string(), | |
450 | + key, | |
451 | + }) | |
452 | +} | |
453 | Index: cargo/src/cargo/sources/git/mod.rs | |
454 | =================================================================== | |
455 | --- cargo.orig/src/cargo/sources/git/mod.rs | |
456 | +++ cargo/src/cargo/sources/git/mod.rs | |
457 | @@ -1,4 +1,5 @@ | |
458 | pub use self::source::GitSource; | |
459 | pub use self::utils::{fetch, GitCheckout, GitDatabase, GitRemote}; | |
460 | +mod known_hosts; | |
461 | mod source; | |
462 | mod utils; | |
463 | Index: cargo/src/cargo/sources/git/utils.rs | |
464 | =================================================================== | |
465 | --- cargo.orig/src/cargo/sources/git/utils.rs | |
466 | +++ cargo/src/cargo/sources/git/utils.rs | |
467 | @@ -647,7 +647,6 @@ where | |
468 | | ErrorClass::Submodule | |
469 | | ErrorClass::FetchHead | |
470 | | ErrorClass::Ssh | |
471 | - | ErrorClass::Callback | |
472 | | ErrorClass::Http => { | |
473 | let mut msg = "network failure seems to have happened\n".to_string(); | |
474 | msg.push_str( | |
475 | @@ -658,6 +657,13 @@ where | |
476 | ); | |
477 | err = err.context(msg); | |
478 | } | |
479 | + ErrorClass::Callback => { | |
480 | + // This unwraps the git2 error. We're using the callback error | |
481 | + // specifically to convey errors from Rust land through the C | |
482 | + // callback interface. We don't need the `; class=Callback | |
483 | + // (26)` that gets tacked on to the git2 error message. | |
484 | + err = anyhow::format_err!("{}", e.message()); | |
485 | + } | |
486 | _ => {} | |
487 | } | |
488 | } | |
489 | @@ -686,12 +692,16 @@ pub fn with_fetch_options( | |
490 | let mut progress = Progress::new("Fetch", config); | |
491 | network::with_retry(config, || { | |
492 | with_authentication(url, git_config, |f| { | |
493 | + let port = Url::parse(url).ok().and_then(|url| url.port()); | |
494 | let mut last_update = Instant::now(); | |
495 | let mut rcb = git2::RemoteCallbacks::new(); | |
496 | // We choose `N=10` here to make a `300ms * 10slots ~= 3000ms` | |
497 | // sliding window for tracking the data transfer rate (in bytes/s). | |
498 | let mut counter = MetricsCounter::<10>::new(0, last_update); | |
499 | rcb.credentials(f); | |
500 | + rcb.certificate_check(|cert, host| { | |
501 | + super::known_hosts::certificate_check(cert, host, port) | |
502 | + }); | |
503 | rcb.transfer_progress(|stats| { | |
504 | let indexed_deltas = stats.indexed_deltas(); | |
505 | let msg = if indexed_deltas > 0 { | |
506 | Index: cargo/src/doc/src/appendix/git-authentication.md | |
507 | =================================================================== | |
508 | --- cargo.orig/src/doc/src/appendix/git-authentication.md | |
509 | +++ cargo/src/doc/src/appendix/git-authentication.md | |
510 | @@ -58,9 +58,32 @@ on how to start `ssh-agent` and to add k | |
511 | > used by Cargo's built-in SSH library. More advanced requirements should use | |
512 | > [`net.git-fetch-with-cli`]. | |
513 | ||
514 | +### SSH Known Hosts | |
515 | + | |
516 | +When connecting to an SSH host, Cargo must verify the identity of the host | |
517 | +using "known hosts", which are a list of host keys. Cargo can look for these | |
518 | +known hosts in OpenSSH-style `known_hosts` files located in their standard | |
519 | +locations (`.ssh/known_hosts` in your home directory, or | |
520 | +`/etc/ssh/ssh_known_hosts` on Unix-like platforms or | |
521 | +`%PROGRAMDATA%\ssh\ssh_known_hosts` on Windows). More information about these | |
522 | +files can be found in the [sshd man page]. | |
523 | + | |
524 | +When connecting to an SSH host before the known hosts has been configured, | |
525 | +Cargo will display an error message instructing you how to add the host key. | |
526 | +This also includes a "fingerprint", which is a smaller hash of the host key, | |
527 | +which should be easier to visually verify. The server administrator can get | |
528 | +the fingerprint by running `ssh-keygen` against the public key (for example, | |
529 | +`ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub`). Well-known sites may | |
530 | +publish their fingerprints on the web; for example GitHub posts theirs at | |
531 | +<https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>. | |
532 | + | |
533 | +Cargo comes with the host keys for [github.com](https://github.com) built-in. | |
534 | +If those ever change, you can add the new keys to your known_hosts file. | |
535 | + | |
536 | [`credential.helper`]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage | |
537 | [`net.git-fetch-with-cli`]: ../reference/config.md#netgit-fetch-with-cli | |
538 | [GCM]: https://github.com/microsoft/Git-Credential-Manager-Core/ | |
539 | [PuTTY]: https://www.chiark.greenend.org.uk/~sgtatham/putty/ | |
540 | [Microsoft installation documentation]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse | |
541 | [key management]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement | |
542 | +[sshd man page]: https://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT | |
543 | --- rust-cargo-0.66.0.orig/Cargo.toml | |
544 | +++ rust-cargo-0.66.0/Cargo.toml | |
545 | @@ -38,6 +38,9 @@ version = "1.0" | |
546 | [dependencies.atty] | |
547 | version = "0.2" | |
548 | ||
549 | +[dependencies.base64] | |
550 | +version = "0.13.1" | |
551 | + | |
552 | [dependencies.bytesize] | |
553 | version = "1.0" | |
554 | ||
555 | @@ -72,10 +75,10 @@ features = ["zlib"] | |
556 | default-features = false | |
557 | ||
558 | [dependencies.git2] | |
559 | -version = "0.15.0" | |
560 | +version = "0.16.0" | |
561 | ||
562 | [dependencies.git2-curl] | |
563 | -version = "0.16.0" | |
564 | +version = "0.17.0" | |
565 | ||
566 | [dependencies.glob] | |
567 | version = "0.3.0" | |
568 | @@ -114,7 +120,7 @@ version = "1.2.0" | |
569 | version = "0.2" | |
570 | ||
571 | [dependencies.libgit2-sys] | |
572 | -version = "0.14.0" | |
573 | +version = "0.14.1" | |
574 | ||
575 | [dependencies.log] | |
576 | version = "0.4.6" |
+157
-0
0 | commit 9f62f8440e9e542f27d60c75be38ac51186c6c32 | |
1 | Author: Eric Huss <eric@huss.org> | |
2 | Date: Fri Dec 9 20:03:27 2022 -0800 | |
3 | ||
4 | Add support for deserializing Vec<Value<String>> in config. | |
5 | ||
6 | This adds the ability to track the definition location of a string | |
7 | in a TOML array. | |
8 | ||
9 | diff --git a/src/cargo/util/config/de.rs b/src/cargo/util/config/de.rs | |
10 | index 6fddc7e71f..1408f15b57 100644 | |
11 | --- a/src/cargo/util/config/de.rs | |
12 | +++ b/src/cargo/util/config/de.rs | |
13 | @@ -384,7 +384,12 @@ impl<'de> de::SeqAccess<'de> for ConfigSeqAccess { | |
14 | { | |
15 | match self.list_iter.next() { | |
16 | // TODO: add `def` to error? | |
17 | - Some((value, _def)) => seed.deserialize(value.into_deserializer()).map(Some), | |
18 | + Some((value, def)) => { | |
19 | + // This might be a String or a Value<String>. | |
20 | + // ValueDeserializer will handle figuring out which one it is. | |
21 | + let maybe_value_de = ValueDeserializer::new_with_string(value, def); | |
22 | + seed.deserialize(maybe_value_de).map(Some) | |
23 | + } | |
24 | None => Ok(None), | |
25 | } | |
26 | } | |
27 | @@ -400,7 +405,17 @@ impl<'de> de::SeqAccess<'de> for ConfigSeqAccess { | |
28 | struct ValueDeserializer<'config> { | |
29 | hits: u32, | |
30 | definition: Definition, | |
31 | - de: Deserializer<'config>, | |
32 | + /// The deserializer, used to actually deserialize a Value struct. | |
33 | + /// This is `None` if deserializing a string. | |
34 | + de: Option<Deserializer<'config>>, | |
35 | + /// A string value to deserialize. | |
36 | + /// | |
37 | + /// This is used for situations where you can't address a string via a | |
38 | + /// TOML key, such as a string inside an array. The `ConfigSeqAccess` | |
39 | + /// doesn't know if the type it should deserialize to is a `String` or | |
40 | + /// `Value<String>`, so `ValueDeserializer` needs to be able to handle | |
41 | + /// both. | |
42 | + str_value: Option<String>, | |
43 | } | |
44 | ||
45 | impl<'config> ValueDeserializer<'config> { | |
46 | @@ -428,9 +443,19 @@ impl<'config> ValueDeserializer<'config> { | |
47 | Ok(ValueDeserializer { | |
48 | hits: 0, | |
49 | definition, | |
50 | - de, | |
51 | + de: Some(de), | |
52 | + str_value: None, | |
53 | }) | |
54 | } | |
55 | + | |
56 | + fn new_with_string(s: String, definition: Definition) -> ValueDeserializer<'config> { | |
57 | + ValueDeserializer { | |
58 | + hits: 0, | |
59 | + definition, | |
60 | + de: None, | |
61 | + str_value: Some(s), | |
62 | + } | |
63 | + } | |
64 | } | |
65 | ||
66 | impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> { | |
67 | @@ -459,9 +484,14 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> { | |
68 | // If this is the first time around we deserialize the `value` field | |
69 | // which is the actual deserializer | |
70 | if self.hits == 1 { | |
71 | - return seed | |
72 | - .deserialize(self.de.clone()) | |
73 | - .map_err(|e| e.with_key_context(&self.de.key, self.definition.clone())); | |
74 | + if let Some(de) = &self.de { | |
75 | + return seed | |
76 | + .deserialize(de.clone()) | |
77 | + .map_err(|e| e.with_key_context(&de.key, self.definition.clone())); | |
78 | + } else { | |
79 | + return seed | |
80 | + .deserialize(self.str_value.as_ref().unwrap().clone().into_deserializer()); | |
81 | + } | |
82 | } | |
83 | ||
84 | // ... otherwise we're deserializing the `definition` field, so we need | |
85 | @@ -484,6 +514,71 @@ impl<'de, 'config> de::MapAccess<'de> for ValueDeserializer<'config> { | |
86 | } | |
87 | } | |
88 | ||
89 | +// Deserializer is only implemented to handle deserializing a String inside a | |
90 | +// sequence (like `Vec<String>` or `Vec<Value<String>>`). `Value<String>` is | |
91 | +// handled by deserialize_struct, and the plain `String` is handled by all the | |
92 | +// other functions here. | |
93 | +impl<'de, 'config> de::Deserializer<'de> for ValueDeserializer<'config> { | |
94 | + type Error = ConfigError; | |
95 | + | |
96 | + fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error> | |
97 | + where | |
98 | + V: de::Visitor<'de>, | |
99 | + { | |
100 | + visitor.visit_str(&self.str_value.expect("string expected")) | |
101 | + } | |
102 | + | |
103 | + fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error> | |
104 | + where | |
105 | + V: de::Visitor<'de>, | |
106 | + { | |
107 | + visitor.visit_string(self.str_value.expect("string expected")) | |
108 | + } | |
109 | + | |
110 | + fn deserialize_struct<V>( | |
111 | + self, | |
112 | + name: &'static str, | |
113 | + fields: &'static [&'static str], | |
114 | + visitor: V, | |
115 | + ) -> Result<V::Value, Self::Error> | |
116 | + where | |
117 | + V: de::Visitor<'de>, | |
118 | + { | |
119 | + // Match on the magical struct name/field names that are passed in to | |
120 | + // detect when we're deserializing `Value<T>`. | |
121 | + // | |
122 | + // See more comments in `value.rs` for the protocol used here. | |
123 | + if name == value::NAME && fields == value::FIELDS { | |
124 | + return visitor.visit_map(self); | |
125 | + } | |
126 | + unimplemented!("only strings and Value can be deserialized from a sequence"); | |
127 | + } | |
128 | + | |
129 | + fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error> | |
130 | + where | |
131 | + V: de::Visitor<'de>, | |
132 | + { | |
133 | + visitor.visit_string(self.str_value.expect("string expected")) | |
134 | + } | |
135 | + | |
136 | + fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error> | |
137 | + where | |
138 | + V: de::Visitor<'de>, | |
139 | + { | |
140 | + visitor.visit_unit() | |
141 | + } | |
142 | + | |
143 | + serde::forward_to_deserialize_any! { | |
144 | + i8 i16 i32 i64 | |
145 | + u8 u16 u32 u64 | |
146 | + option | |
147 | + newtype_struct seq tuple tuple_struct map enum bool | |
148 | + f32 f64 char bytes | |
149 | + byte_buf unit unit_struct | |
150 | + identifier | |
151 | + } | |
152 | +} | |
153 | + | |
154 | /// A deserializer which takes two values and deserializes into a tuple of those | |
155 | /// two values. This is similar to types like `StrDeserializer` in upstream | |
156 | /// serde itself. |
+299
-0
0 | commit 026bda3fb5eddac0df111ee150706f756558a7b3 | |
1 | Author: Eric Huss <eric@huss.org> | |
2 | Date: Fri Dec 9 20:38:12 2022 -0800 | |
3 | ||
4 | Support configuring ssh known-hosts via cargo config. | |
5 | ||
6 | diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs | |
7 | index 875dcf63f3..7efea43c3b 100644 | |
8 | --- a/src/cargo/sources/git/known_hosts.rs | |
9 | +++ b/src/cargo/sources/git/known_hosts.rs | |
10 | @@ -20,6 +20,7 @@ | |
11 | //! hostname patterns, and revoked markers. See "FIXME" comments littered in | |
12 | //! this file. | |
13 | ||
14 | +use crate::util::config::{Definition, Value}; | |
15 | use git2::cert::Cert; | |
16 | use git2::CertificateCheckStatus; | |
17 | use std::collections::HashSet; | |
18 | @@ -74,6 +75,8 @@ impl From<anyhow::Error> for KnownHostError { | |
19 | enum KnownHostLocation { | |
20 | /// Loaded from a file from disk. | |
21 | File { path: PathBuf, lineno: u32 }, | |
22 | + /// Loaded from cargo's config system. | |
23 | + Config { definition: Definition }, | |
24 | /// Part of the hard-coded bundled keys in Cargo. | |
25 | Bundled, | |
26 | } | |
27 | @@ -83,6 +86,8 @@ pub fn certificate_check( | |
28 | cert: &Cert<'_>, | |
29 | host: &str, | |
30 | port: Option<u16>, | |
31 | + config_known_hosts: Option<&Vec<Value<String>>>, | |
32 | + diagnostic_home_config: &str, | |
33 | ) -> Result<CertificateCheckStatus, git2::Error> { | |
34 | let Some(host_key) = cert.as_hostkey() else { | |
35 | // Return passthrough for TLS X509 certificates to use whatever validation | |
36 | @@ -96,7 +101,7 @@ pub fn certificate_check( | |
37 | _ => host.to_string(), | |
38 | }; | |
39 | // The error message must be constructed as a string to pass through the libgit2 C API. | |
40 | - let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port) { | |
41 | + let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port, config_known_hosts) { | |
42 | Ok(()) => { | |
43 | return Ok(CertificateCheckStatus::CertificateOk); | |
44 | } | |
45 | @@ -113,13 +118,13 @@ pub fn certificate_check( | |
46 | // Try checking without the port. | |
47 | if port.is_some() | |
48 | && !matches!(port, Some(22)) | |
49 | - && check_ssh_known_hosts(host_key, host).is_ok() | |
50 | + && check_ssh_known_hosts(host_key, host, config_known_hosts).is_ok() | |
51 | { | |
52 | return Ok(CertificateCheckStatus::CertificateOk); | |
53 | } | |
54 | let key_type_short_name = key_type.short_name(); | |
55 | let key_type_name = key_type.name(); | |
56 | - let known_hosts_location = user_known_host_location_to_add(); | |
57 | + let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config); | |
58 | let other_hosts_message = if other_hosts.is_empty() { | |
59 | String::new() | |
60 | } else { | |
61 | @@ -132,6 +137,9 @@ pub fn certificate_check( | |
62 | KnownHostLocation::File { path, lineno } => { | |
63 | format!("{} line {lineno}", path.display()) | |
64 | } | |
65 | + KnownHostLocation::Config { definition } => { | |
66 | + format!("config value from {definition}") | |
67 | + } | |
68 | KnownHostLocation::Bundled => format!("bundled with cargo"), | |
69 | }; | |
70 | write!(msg, " {loc}: {}\n", known_host.patterns).unwrap(); | |
71 | @@ -163,7 +171,7 @@ pub fn certificate_check( | |
72 | }) => { | |
73 | let key_type_short_name = key_type.short_name(); | |
74 | let key_type_name = key_type.name(); | |
75 | - let known_hosts_location = user_known_host_location_to_add(); | |
76 | + let known_hosts_location = user_known_host_location_to_add(diagnostic_home_config); | |
77 | let old_key_resolution = match old_known_host.location { | |
78 | KnownHostLocation::File { path, lineno } => { | |
79 | let old_key_location = path.display(); | |
80 | @@ -173,6 +181,13 @@ pub fn certificate_check( | |
81 | and adding the new key to {known_hosts_location}", | |
82 | ) | |
83 | } | |
84 | + KnownHostLocation::Config { definition } => { | |
85 | + format!( | |
86 | + "removing the old {key_type_name} key for `{hostname}` \ | |
87 | + loaded from Cargo's config at {definition}, \ | |
88 | + and adding the new key to {known_hosts_location}" | |
89 | + ) | |
90 | + } | |
91 | KnownHostLocation::Bundled => { | |
92 | format!( | |
93 | "adding the new key to {known_hosts_location}\n\ | |
94 | @@ -217,6 +232,7 @@ pub fn certificate_check( | |
95 | fn check_ssh_known_hosts( | |
96 | cert_host_key: &git2::cert::CertHostkey<'_>, | |
97 | host: &str, | |
98 | + config_known_hosts: Option<&Vec<Value<String>>>, | |
99 | ) -> Result<(), KnownHostError> { | |
100 | let Some(remote_host_key) = cert_host_key.hostkey() else { | |
101 | return Err(anyhow::format_err!("remote host key is not available").into()); | |
102 | @@ -237,6 +253,23 @@ fn check_ssh_known_hosts( | |
103 | let hosts = load_hostfile(&path)?; | |
104 | known_hosts.extend(hosts); | |
105 | } | |
106 | + if let Some(config_known_hosts) = config_known_hosts { | |
107 | + // Format errors aren't an error in case the format needs to change in | |
108 | + // the future, to retain forwards compatibility. | |
109 | + for line_value in config_known_hosts { | |
110 | + let location = KnownHostLocation::Config { | |
111 | + definition: line_value.definition.clone(), | |
112 | + }; | |
113 | + match parse_known_hosts_line(&line_value.val, location) { | |
114 | + Some(known_host) => known_hosts.push(known_host), | |
115 | + None => log::warn!( | |
116 | + "failed to parse known host {} from {}", | |
117 | + line_value.val, | |
118 | + line_value.definition | |
119 | + ), | |
120 | + } | |
121 | + } | |
122 | + } | |
123 | // Load the bundled keys. Don't add keys for hosts that the user has | |
124 | // configured, which gives them the option to override them. This could be | |
125 | // useful if the keys are ever revoked. | |
126 | @@ -363,12 +396,18 @@ fn user_known_host_location() -> Option<PathBuf> { | |
127 | ||
128 | /// The location to display in an error message instructing the user where to | |
129 | /// add the new key. | |
130 | -fn user_known_host_location_to_add() -> String { | |
131 | +fn user_known_host_location_to_add(diagnostic_home_config: &str) -> String { | |
132 | // Note that we don't bother with the legacy known_hosts2 files. | |
133 | - match user_known_host_location() { | |
134 | - Some(path) => path.to_str().expect("utf-8 home").to_string(), | |
135 | - None => "~/.ssh/known_hosts".to_string(), | |
136 | - } | |
137 | + let user = user_known_host_location(); | |
138 | + let openssh_loc = match &user { | |
139 | + Some(path) => path.to_str().expect("utf-8 home"), | |
140 | + None => "~/.ssh/known_hosts", | |
141 | + }; | |
142 | + format!( | |
143 | + "the `net.ssh.known-hosts` array in your Cargo configuration \ | |
144 | + (such as {diagnostic_home_config}) \ | |
145 | + or in your OpenSSH known_hosts file at {openssh_loc}" | |
146 | + ) | |
147 | } | |
148 | ||
149 | /// A single known host entry. | |
150 | diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs | |
151 | index 831c43be6b..457c97c5bb 100644 | |
152 | --- a/src/cargo/sources/git/utils.rs | |
153 | +++ b/src/cargo/sources/git/utils.rs | |
154 | @@ -726,6 +726,9 @@ pub fn with_fetch_options( | |
155 | cb: &mut dyn FnMut(git2::FetchOptions<'_>) -> CargoResult<()>, | |
156 | ) -> CargoResult<()> { | |
157 | let mut progress = Progress::new("Fetch", config); | |
158 | + let ssh_config = config.net_config()?.ssh.as_ref(); | |
159 | + let config_known_hosts = ssh_config.and_then(|ssh| ssh.known_hosts.as_ref()); | |
160 | + let diagnostic_home_config = config.diagnostic_home_config(); | |
161 | network::with_retry(config, || { | |
162 | with_authentication(url, git_config, |f| { | |
163 | let port = Url::parse(url).ok().and_then(|url| url.port()); | |
164 | @@ -736,7 +739,13 @@ pub fn with_fetch_options( | |
165 | let mut counter = MetricsCounter::<10>::new(0, last_update); | |
166 | rcb.credentials(f); | |
167 | rcb.certificate_check(|cert, host| { | |
168 | - super::known_hosts::certificate_check(cert, host, port) | |
169 | + super::known_hosts::certificate_check( | |
170 | + cert, | |
171 | + host, | |
172 | + port, | |
173 | + config_known_hosts, | |
174 | + &diagnostic_home_config, | |
175 | + ) | |
176 | }); | |
177 | rcb.transfer_progress(|stats| { | |
178 | let indexed_deltas = stats.indexed_deltas(); | |
179 | diff --git a/src/cargo/util/config/mod.rs b/src/cargo/util/config/mod.rs | |
180 | index d30e094413..d9ab142c4e 100644 | |
181 | --- a/src/cargo/util/config/mod.rs | |
182 | +++ b/src/cargo/util/config/mod.rs | |
183 | @@ -356,6 +356,18 @@ impl Config { | |
184 | &self.home_path | |
185 | } | |
186 | ||
187 | + /// Returns a path to display to the user with the location of their home | |
188 | + /// config file (to only be used for displaying a diagnostics suggestion, | |
189 | + /// such as recommending where to add a config value). | |
190 | + pub fn diagnostic_home_config(&self) -> String { | |
191 | + let home = self.home_path.as_path_unlocked(); | |
192 | + let path = match self.get_file_path(home, "config", false) { | |
193 | + Ok(Some(existing_path)) => existing_path, | |
194 | + _ => home.join("config.toml"), | |
195 | + }; | |
196 | + path.to_string_lossy().to_string() | |
197 | + } | |
198 | + | |
199 | /// Gets the Cargo Git directory (`<cargo_home>/git`). | |
200 | pub fn git_path(&self) -> Filesystem { | |
201 | self.home_path.join("git") | |
202 | @@ -2356,6 +2368,13 @@ pub struct CargoNetConfig { | |
203 | pub retry: Option<u32>, | |
204 | pub offline: Option<bool>, | |
205 | pub git_fetch_with_cli: Option<bool>, | |
206 | + pub ssh: Option<CargoSshConfig>, | |
207 | +} | |
208 | + | |
209 | +#[derive(Debug, Deserialize)] | |
210 | +#[serde(rename_all = "kebab-case")] | |
211 | +pub struct CargoSshConfig { | |
212 | + pub known_hosts: Option<Vec<Value<String>>>, | |
213 | } | |
214 | ||
215 | #[derive(Debug, Deserialize)] | |
216 | diff --git a/src/doc/src/appendix/git-authentication.md b/src/doc/src/appendix/git-authentication.md | |
217 | index a7db1ac7f1..f46a6535c6 100644 | |
218 | --- a/src/doc/src/appendix/git-authentication.md | |
219 | +++ b/src/doc/src/appendix/git-authentication.md | |
220 | @@ -66,7 +66,8 @@ known hosts in OpenSSH-style `known_hosts` files located in their standard | |
221 | locations (`.ssh/known_hosts` in your home directory, or | |
222 | `/etc/ssh/ssh_known_hosts` on Unix-like platforms or | |
223 | `%PROGRAMDATA%\ssh\ssh_known_hosts` on Windows). More information about these | |
224 | -files can be found in the [sshd man page]. | |
225 | +files can be found in the [sshd man page]. Alternatively, keys may be | |
226 | +configured in a Cargo configuration file with [`net.ssh.known-hosts`]. | |
227 | ||
228 | When connecting to an SSH host before the known hosts has been configured, | |
229 | Cargo will display an error message instructing you how to add the host key. | |
230 | @@ -78,10 +79,11 @@ publish their fingerprints on the web; for example GitHub posts theirs at | |
231 | <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints>. | |
232 | ||
233 | Cargo comes with the host keys for [github.com](https://github.com) built-in. | |
234 | -If those ever change, you can add the new keys to your known_hosts file. | |
235 | +If those ever change, you can add the new keys to the config or known_hosts file. | |
236 | ||
237 | [`credential.helper`]: https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage | |
238 | [`net.git-fetch-with-cli`]: ../reference/config.md#netgit-fetch-with-cli | |
239 | +[`net.ssh.known-hosts`]: ../reference/config.md#netsshknown-hosts | |
240 | [GCM]: https://github.com/microsoft/Git-Credential-Manager-Core/ | |
241 | [PuTTY]: https://www.chiark.greenend.org.uk/~sgtatham/putty/ | |
242 | [Microsoft installation documentation]: https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse | |
243 | diff --git a/src/doc/src/reference/config.md b/src/doc/src/reference/config.md | |
244 | index 1e50648797..f804ceebea 100644 | |
245 | --- a/src/doc/src/reference/config.md | |
246 | +++ b/src/doc/src/reference/config.md | |
247 | @@ -114,6 +114,9 @@ retry = 2 # network retries | |
248 | git-fetch-with-cli = true # use the `git` executable for git operations | |
249 | offline = true # do not access the network | |
250 | ||
251 | +[net.ssh] | |
252 | +known-hosts = ["..."] # known SSH host keys | |
253 | + | |
254 | [patch.<registry>] | |
255 | # Same keys as for [patch] in Cargo.toml | |
256 | ||
257 | @@ -750,6 +753,41 @@ needed, and generate an error if it encounters a network error. | |
258 | ||
259 | Can be overridden with the `--offline` command-line option. | |
260 | ||
261 | +##### `net.ssh` | |
262 | + | |
263 | +The `[net.ssh]` table contains settings for SSH connections. | |
264 | + | |
265 | +##### `net.ssh.known-hosts` | |
266 | +* Type: array of strings | |
267 | +* Default: see description | |
268 | +* Environment: not supported | |
269 | + | |
270 | +The `known-hosts` array contains a list of SSH host keys that should be | |
271 | +accepted as valid when connecting to an SSH server (such as for SSH git | |
272 | +dependencies). Each entry should be a string in a format similar to OpenSSH | |
273 | +`known_hosts` files. Each string should start with one or more hostnames | |
274 | +separated by commas, a space, the key type name, a space, and the | |
275 | +base64-encoded key. For example: | |
276 | + | |
277 | +```toml | |
278 | +[net.ssh] | |
279 | +known-hosts = [ | |
280 | + "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFO4Q5T0UV0SQevair9PFwoxY9dl4pQl3u5phoqJH3cF" | |
281 | +] | |
282 | +``` | |
283 | + | |
284 | +Cargo will attempt to load known hosts keys from common locations supported in | |
285 | +OpenSSH, and will join those with any listed in a Cargo configuration file. | |
286 | +If any matching entry has the correct key, the connection will be allowed. | |
287 | + | |
288 | +Cargo comes with the host keys for [github.com][github-keys] built-in. If | |
289 | +those ever change, you can add the new keys to the config or known_hosts file. | |
290 | + | |
291 | +See [Git Authentication](../appendix/git-authentication.md#ssh-known-hosts) | |
292 | +for more details. | |
293 | + | |
294 | +[github-keys]: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints | |
295 | + | |
296 | #### `[patch]` | |
297 | ||
298 | Just as you can override dependencies using [`[patch]` in |
+244
-0
0 | commit 302a543ddf3b7621c2f10623862029d35fae7e3c | |
1 | Author: Eric Huss <eric@huss.org> | |
2 | Date: Mon Dec 12 20:14:23 2022 -0800 | |
3 | ||
4 | Add some known_hosts tests. | |
5 | ||
6 | This also fixes a bug with the host matching when there are comma-separated hosts. | |
7 | ||
8 | diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs | |
9 | index 7efea43c3b..58e64e7913 100644 | |
10 | --- a/src/cargo/sources/git/known_hosts.rs | |
11 | +++ b/src/cargo/sources/git/known_hosts.rs | |
12 | @@ -21,7 +21,7 @@ | |
13 | //! this file. | |
14 | ||
15 | use crate::util::config::{Definition, Value}; | |
16 | -use git2::cert::Cert; | |
17 | +use git2::cert::{Cert, SshHostKeyType}; | |
18 | use git2::CertificateCheckStatus; | |
19 | use std::collections::HashSet; | |
20 | use std::fmt::Write; | |
21 | @@ -49,7 +49,7 @@ enum KnownHostError { | |
22 | /// The host key was not found. | |
23 | HostKeyNotFound { | |
24 | hostname: String, | |
25 | - key_type: git2::cert::SshHostKeyType, | |
26 | + key_type: SshHostKeyType, | |
27 | remote_host_key: String, | |
28 | remote_fingerprint: String, | |
29 | other_hosts: Vec<KnownHost>, | |
30 | @@ -57,7 +57,7 @@ enum KnownHostError { | |
31 | /// The host key was found, but does not match the remote's key. | |
32 | HostKeyHasChanged { | |
33 | hostname: String, | |
34 | - key_type: git2::cert::SshHostKeyType, | |
35 | + key_type: SshHostKeyType, | |
36 | old_known_host: KnownHost, | |
37 | remote_host_key: String, | |
38 | remote_fingerprint: String, | |
39 | @@ -238,11 +238,6 @@ fn check_ssh_known_hosts( | |
40 | return Err(anyhow::format_err!("remote host key is not available").into()); | |
41 | }; | |
42 | let remote_key_type = cert_host_key.hostkey_type().unwrap(); | |
43 | - // `changed_key` keeps track of any entries where the key has changed. | |
44 | - let mut changed_key = None; | |
45 | - // `other_hosts` keeps track of any entries that have an identical key, | |
46 | - // but a different hostname. | |
47 | - let mut other_hosts = Vec::new(); | |
48 | ||
49 | // Collect all the known host entries from disk. | |
50 | let mut known_hosts = Vec::new(); | |
51 | @@ -293,6 +288,21 @@ fn check_ssh_known_hosts( | |
52 | }); | |
53 | } | |
54 | } | |
55 | + check_ssh_known_hosts_loaded(&known_hosts, host, remote_key_type, remote_host_key) | |
56 | +} | |
57 | + | |
58 | +/// Checks a host key against a loaded set of known hosts. | |
59 | +fn check_ssh_known_hosts_loaded( | |
60 | + known_hosts: &[KnownHost], | |
61 | + host: &str, | |
62 | + remote_key_type: SshHostKeyType, | |
63 | + remote_host_key: &[u8], | |
64 | +) -> Result<(), KnownHostError> { | |
65 | + // `changed_key` keeps track of any entries where the key has changed. | |
66 | + let mut changed_key = None; | |
67 | + // `other_hosts` keeps track of any entries that have an identical key, | |
68 | + // but a different hostname. | |
69 | + let mut other_hosts = Vec::new(); | |
70 | ||
71 | for known_host in known_hosts { | |
72 | // The key type from libgit2 needs to match the key type from the host file. | |
73 | @@ -301,7 +311,6 @@ fn check_ssh_known_hosts( | |
74 | } | |
75 | let key_matches = known_host.key == remote_host_key; | |
76 | if !known_host.host_matches(host) { | |
77 | - // `name` can be None for hashed hostnames (which libgit2 does not expose). | |
78 | if key_matches { | |
79 | other_hosts.push(known_host.clone()); | |
80 | } | |
81 | @@ -434,7 +443,7 @@ impl KnownHost { | |
82 | return false; | |
83 | } | |
84 | } else { | |
85 | - match_found = pattern == host; | |
86 | + match_found |= pattern == host; | |
87 | } | |
88 | } | |
89 | match_found | |
90 | @@ -444,6 +453,10 @@ impl KnownHost { | |
91 | /// Loads an OpenSSH known_hosts file. | |
92 | fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> { | |
93 | let contents = cargo_util::paths::read(path)?; | |
94 | + Ok(load_hostfile_contents(path, &contents)) | |
95 | +} | |
96 | + | |
97 | +fn load_hostfile_contents(path: &Path, contents: &str) -> Vec<KnownHost> { | |
98 | let entries = contents | |
99 | .lines() | |
100 | .enumerate() | |
101 | @@ -455,13 +468,13 @@ fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> { | |
102 | parse_known_hosts_line(line, location) | |
103 | }) | |
104 | .collect(); | |
105 | - Ok(entries) | |
106 | + entries | |
107 | } | |
108 | ||
109 | fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> { | |
110 | let line = line.trim(); | |
111 | // FIXME: @revoked and @cert-authority is currently not supported. | |
112 | - if line.is_empty() || line.starts_with('#') || line.starts_with('@') { | |
113 | + if line.is_empty() || line.starts_with(['#', '@', '|']) { | |
114 | return None; | |
115 | } | |
116 | let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty()); | |
117 | @@ -476,3 +489,126 @@ fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<Kno | |
118 | key, | |
119 | }) | |
120 | } | |
121 | + | |
122 | +#[cfg(test)] | |
123 | +mod tests { | |
124 | + use super::*; | |
125 | + | |
126 | + static COMMON_CONTENTS: &str = r#" | |
127 | + # Comments allowed at start of line | |
128 | + | |
129 | + example.com,rust-lang.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC5MzWIpZwpkpDjyCNiTIEVFhSA9OUUQvjFo7CgZBGCAj/cqeUIgiLsgtfmtBsfWIkAECQpM7ePP7NLZFGJcHvoyg5jXJiIX5s0eKo9IlcuTLLrMkW5MkHXE7bNklVbW1WdCfF2+y7Ao25B4L8FFRokMh0yp/H6+8xZ7PdVwL3FRPEg8ftZ5R0kuups6xiMHPRX+f/07vfJzA47YDPmXfhkn+JK8kL0JYw8iy8BtNBfRQL99d9iXJzWXnNce5NHMuKD5rOonD3aQHLDlwK+KhrFRrdaxQEM8ZWxNti0ux8yT4Dl5jJY0CrIu3Xl6+qroVgTqJGNkTbhs5DGWdFh6BLPTTH15rN4buisg7uMyLyHqx06ckborqD33gWu+Jig7O+PV6KJmL5mp1O1HXvZqkpBdTiT6GiDKG3oECCIXkUk0BSU9VG9VQcrMxxvgiHlyoXUAfYQoXv/lnxkTnm+Sr36kutsVOs7n5B43ZKAeuaxyQ11huJZpxamc0RA1HM641s= eric@host | |
130 | + Example.net ssh-dss AAAAB3NzaC1kc3MAAACBAK2Ek3jVxisXmz5UcZ7W65BAj/nDJCCVvSe0Aytndn4PH6k7sVesut5OoY6PdksZ9tEfuFjjS9HR5SJb8j1GW0GxtaSHHbf+rNc36PeU75bffzyIWwpA8uZFONt5swUAXJXcsHOoapNbUFuhHsRhB2hXxz9QGNiiwIwRJeSHixKRAAAAFQChKfxO1z9H2/757697xP5nJ/Z5dwAAAIEAoc+HIWas+4WowtB/KtAp6XE0B9oHI+55wKtdcGwwb7zHKK9scWNXwxIcMhSvyB3Oe2I7dQQlvyIWxsdZlzOkX0wdsTHjIAnBAP68MyvMv4kq3+I5GAVcFsqoLZfZvh0dlcgUq1/YNYZwKlt89tnzk8Fp4KLWmuw8Bd8IShYVa78AAACAL3qd8kNTY7CthgsQ8iWdjbkGSF/1KCeFyt8UjurInp9wvPDjqagwakbyLOzN7y3/ItTPCaGuX+RjFP0zZTf8i9bsAVyjFJiJ7vzRXcWytuFWANrpzLTn1qzPfh63iK92Aw8AVBYvEA/4bxo+XReAvhNBB/m78G6OedTeu6ZoTsI= eric@host | |
131 | + [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 | |
132 | + nistp256.example.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ4iYGCcJrUIfrHfzlsv8e8kaF36qpcUpe3VNAKVCZX/BDptIdlEe8u8vKNRTPgUO9jqS0+tjTcPiQd8/8I9qng= eric@host | |
133 | + nistp384.example.org ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBNuGT3TqMz2rcwOt2ZqkiNqq7dvWPE66W2qPCoZsh0pQhVU3BnhKIc6nEr6+Wts0Z3jdF3QWwxbbTjbVTVhdr8fMCFhDCWiQFm9xLerYPKnu9qHvx9K87/fjc5+0pu4hLA== eric@host | |
134 | + nistp521.example.org ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAD35HH6OsK4DN75BrKipVj/GvZaUzjPNa1F8wMjUdPB1JlVcUfgzJjWSxrhmaNN3u0soiZw8WNRFINsGPCw5E7DywF1689WcIj2Ye2rcy99je15FknScTzBBD04JgIyOI50mCUaPCBoF14vFlN6BmO00cFo+yzy5N8GuQ2sx9kr21xmFQ== eric@host | |
135 | + # Revoked not yet supported. | |
136 | + @revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host | |
137 | + example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host | |
138 | + 192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host | |
139 | + # Hash not yet supported. | |
140 | + |1|7CMSYgzdwruFLRhwowMtKx0maIE=|Tlff1GFqc3Ao+fUWxMEVG8mJiyk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host | |
141 | + # Negation isn't terribly useful without globs. | |
142 | + neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host | |
143 | + "#; | |
144 | + | |
145 | + #[test] | |
146 | + fn known_hosts_parse() { | |
147 | + let kh_path = Path::new("/home/abc/.known_hosts"); | |
148 | + let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS); | |
149 | + assert_eq!(khs.len(), 9); | |
150 | + match &khs[0].location { | |
151 | + KnownHostLocation::File { path, lineno } => { | |
152 | + assert_eq!(path, kh_path); | |
153 | + assert_eq!(*lineno, 4); | |
154 | + } | |
155 | + _ => panic!("unexpected"), | |
156 | + } | |
157 | + assert_eq!(khs[0].patterns, "example.com,rust-lang.org"); | |
158 | + assert_eq!(khs[0].key_type, "ssh-rsa"); | |
159 | + assert_eq!(khs[0].key.len(), 407); | |
160 | + 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)"); | |
161 | + match &khs[1].location { | |
162 | + KnownHostLocation::File { path, lineno } => { | |
163 | + assert_eq!(path, kh_path); | |
164 | + assert_eq!(*lineno, 5); | |
165 | + } | |
166 | + _ => panic!("unexpected"), | |
167 | + } | |
168 | + assert_eq!(khs[2].patterns, "[example.net]:2222"); | |
169 | + assert_eq!(khs[3].patterns, "nistp256.example.org"); | |
170 | + assert_eq!(khs[7].patterns, "192.168.42.12"); | |
171 | + } | |
172 | + | |
173 | + #[test] | |
174 | + fn host_matches() { | |
175 | + let kh_path = Path::new("/home/abc/.known_hosts"); | |
176 | + let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS); | |
177 | + assert!(khs[0].host_matches("example.com")); | |
178 | + assert!(khs[0].host_matches("rust-lang.org")); | |
179 | + assert!(khs[0].host_matches("EXAMPLE.COM")); | |
180 | + assert!(khs[1].host_matches("example.net")); | |
181 | + assert!(!khs[0].host_matches("example.net")); | |
182 | + assert!(khs[2].host_matches("[example.net]:2222")); | |
183 | + assert!(!khs[2].host_matches("example.net")); | |
184 | + assert!(!khs[8].host_matches("neg.example.com")); | |
185 | + } | |
186 | + | |
187 | + #[test] | |
188 | + fn check_match() { | |
189 | + let kh_path = Path::new("/home/abc/.known_hosts"); | |
190 | + let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS); | |
191 | + | |
192 | + assert!(check_ssh_known_hosts_loaded( | |
193 | + &khs, | |
194 | + "example.com", | |
195 | + SshHostKeyType::Rsa, | |
196 | + &khs[0].key | |
197 | + ) | |
198 | + .is_ok()); | |
199 | + | |
200 | + match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Dss, &khs[0].key) { | |
201 | + Err(KnownHostError::HostKeyNotFound { | |
202 | + hostname, | |
203 | + remote_fingerprint, | |
204 | + other_hosts, | |
205 | + .. | |
206 | + }) => { | |
207 | + assert_eq!( | |
208 | + remote_fingerprint, | |
209 | + "yn+pONDn0EcgdOCVptgB4RZd/wqmsVKrPnQMLtrvhw8" | |
210 | + ); | |
211 | + assert_eq!(hostname, "example.com"); | |
212 | + assert_eq!(other_hosts.len(), 0); | |
213 | + } | |
214 | + _ => panic!("unexpected"), | |
215 | + } | |
216 | + | |
217 | + match check_ssh_known_hosts_loaded( | |
218 | + &khs, | |
219 | + "foo.example.com", | |
220 | + SshHostKeyType::Rsa, | |
221 | + &khs[0].key, | |
222 | + ) { | |
223 | + Err(KnownHostError::HostKeyNotFound { other_hosts, .. }) => { | |
224 | + assert_eq!(other_hosts.len(), 1); | |
225 | + assert_eq!(other_hosts[0].patterns, "example.com,rust-lang.org"); | |
226 | + } | |
227 | + _ => panic!("unexpected"), | |
228 | + } | |
229 | + | |
230 | + let mut modified_key = khs[0].key.clone(); | |
231 | + modified_key[0] = 1; | |
232 | + match check_ssh_known_hosts_loaded(&khs, "example.com", SshHostKeyType::Rsa, &modified_key) | |
233 | + { | |
234 | + Err(KnownHostError::HostKeyHasChanged { old_known_host, .. }) => { | |
235 | + assert!(matches!( | |
236 | + old_known_host.location, | |
237 | + KnownHostLocation::File { lineno: 4, .. } | |
238 | + )); | |
239 | + } | |
240 | + _ => panic!("unexpected"), | |
241 | + } | |
242 | + } | |
243 | +} |
0 | commit cf716fc3c2b0785013b321f08d6cf9e277f89c84 | |
1 | Author: Eric Huss <eric@huss.org> | |
2 | Date: Tue Dec 13 08:14:59 2022 -0800 | |
3 | ||
4 | Remove let-else, just use ? propagation. | |
5 | ||
6 | Co-authored-by: Weihang Lo <weihanglo@users.noreply.github.com> | |
7 | ||
8 | diff --git a/src/cargo/sources/git/known_hosts.rs b/src/cargo/sources/git/known_hosts.rs | |
9 | index 58e64e7913..f272195306 100644 | |
10 | --- a/src/cargo/sources/git/known_hosts.rs | |
11 | +++ b/src/cargo/sources/git/known_hosts.rs | |
12 | @@ -478,10 +478,9 @@ fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<Kno | |
13 | return None; | |
14 | } | |
15 | let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty()); | |
16 | - let Some(patterns) = parts.next() else { return None }; | |
17 | - let Some(key_type) = parts.next() else { return None }; | |
18 | - let Some(key) = parts.next() else { return None }; | |
19 | - let Ok(key) = base64::decode(key) else { return None }; | |
20 | + let patterns = parts.next()?; | |
21 | + let key_type = parts.next()?; | |
22 | + let key = parts.next().map(base64::decode)?.ok()?; | |
23 | Some(KnownHost { | |
24 | location, | |
25 | patterns: patterns.to_string(), |
+79
-0
0 | commit 018403ceaf71e205dbec64698bb864f5e094aec8 | |
1 | Author: Eric Huss <eric@huss.org> | |
2 | Date: Wed Dec 14 19:01:40 2022 -0800 | |
3 | ||
4 | Add test for config Value in TOML array. | |
5 | ||
6 | diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs | |
7 | index b1d07bb405..d1487833f7 100644 | |
8 | --- a/tests/testsuite/config.rs | |
9 | +++ b/tests/testsuite/config.rs | |
10 | @@ -1,7 +1,7 @@ | |
11 | //! Tests for config settings. | |
12 | ||
13 | use cargo::core::{PackageIdSpec, Shell}; | |
14 | -use cargo::util::config::{self, Config, SslVersionConfig, StringList}; | |
15 | +use cargo::util::config::{self, Config, Definition, SslVersionConfig, StringList}; | |
16 | use cargo::util::interning::InternedString; | |
17 | use cargo::util::toml::{self, VecStringOrBool as VSOB}; | |
18 | use cargo::CargoResult; | |
19 | @@ -1508,3 +1508,59 @@ fn all_profile_options() { | |
20 | let roundtrip_toml = toml_edit::easy::to_string(&roundtrip).unwrap(); | |
21 | compare::assert_match_exact(&profile_toml, &roundtrip_toml); | |
22 | } | |
23 | + | |
24 | +#[cargo_test] | |
25 | +fn value_in_array() { | |
26 | + // Value<String> in an array should work | |
27 | + let root_path = paths::root().join(".cargo/config.toml"); | |
28 | + write_config_at( | |
29 | + &root_path, | |
30 | + "\ | |
31 | +[net.ssh] | |
32 | +known-hosts = [ | |
33 | + \"example.com ...\", | |
34 | + \"example.net ...\", | |
35 | +] | |
36 | +", | |
37 | + ); | |
38 | + | |
39 | + let foo_path = paths::root().join("foo/.cargo/config.toml"); | |
40 | + write_config_at( | |
41 | + &foo_path, | |
42 | + "\ | |
43 | +[net.ssh] | |
44 | +known-hosts = [ | |
45 | + \"example.org ...\", | |
46 | +] | |
47 | +", | |
48 | + ); | |
49 | + | |
50 | + let config = ConfigBuilder::new() | |
51 | + .cwd("foo") | |
52 | + // environment variables don't actually work for known-hosts due to | |
53 | + // space splitting, but this is included here just to validate that | |
54 | + // they work (particularly if other Vec<Value> config vars are added | |
55 | + // in the future). | |
56 | + .env("CARGO_NET_SSH_KNOWN_HOSTS", "env-example") | |
57 | + .build(); | |
58 | + let net_config = config.net_config().unwrap(); | |
59 | + let kh = net_config | |
60 | + .ssh | |
61 | + .as_ref() | |
62 | + .unwrap() | |
63 | + .known_hosts | |
64 | + .as_ref() | |
65 | + .unwrap(); | |
66 | + assert_eq!(kh.len(), 4); | |
67 | + assert_eq!(kh[0].val, "example.org ..."); | |
68 | + assert_eq!(kh[0].definition, Definition::Path(foo_path.clone())); | |
69 | + assert_eq!(kh[1].val, "example.com ..."); | |
70 | + assert_eq!(kh[1].definition, Definition::Path(root_path.clone())); | |
71 | + assert_eq!(kh[2].val, "example.net ..."); | |
72 | + assert_eq!(kh[2].definition, Definition::Path(root_path.clone())); | |
73 | + assert_eq!(kh[3].val, "env-example"); | |
74 | + assert_eq!( | |
75 | + kh[3].definition, | |
76 | + Definition::Environment("CARGO_NET_SSH_KNOWN_HOSTS".to_string()) | |
77 | + ); | |
78 | +} |
0 | This patch is based on the upstream commit described below, adapted for use | |
1 | in the Debian package by Peter Michael Green. | |
2 | ||
3 | commit 67ae2dcafea5955824b1f390568a5fa109424987 | |
4 | Author: Eric Huss <eric@huss.org> | |
5 | Date: Wed Dec 28 15:52:10 2022 -0800 | |
6 | ||
7 | ssh known_hosts: support hashed hostnames | |
8 | ||
9 | Index: cargo/src/cargo/sources/git/known_hosts.rs | |
10 | =================================================================== | |
11 | --- cargo.orig/src/cargo/sources/git/known_hosts.rs | |
12 | +++ cargo/src/cargo/sources/git/known_hosts.rs | |
13 | @@ -16,13 +16,13 @@ | |
14 | //! - `VerifyHostKeyDNS` — Uses SSHFP DNS records to fetch a host key. | |
15 | //! | |
16 | //! There's also a number of things that aren't supported but could be easily | |
17 | -//! added (it just adds a little complexity). For example, hashed hostnames, | |
18 | -//! hostname patterns, and revoked markers. See "FIXME" comments littered in | |
19 | -//! this file. | |
20 | +//! added (it just adds a little complexity). For example, hostname patterns, | |
21 | +//! and revoked markers. See "FIXME" comments littered in this file. | |
22 | ||
23 | use crate::util::config::{Definition, Value}; | |
24 | use git2::cert::{Cert, SshHostKeyType}; | |
25 | use git2::CertificateCheckStatus; | |
26 | +use hmac::Mac; | |
27 | use std::collections::HashSet; | |
28 | use std::fmt::Write; | |
29 | use std::path::{Path, PathBuf}; | |
30 | @@ -419,6 +419,8 @@ fn user_known_host_location_to_add(diagn | |
31 | ) | |
32 | } | |
33 | ||
34 | +const HASH_HOSTNAME_PREFIX: &str = "|1|"; | |
35 | + | |
36 | /// A single known host entry. | |
37 | #[derive(Clone)] | |
38 | struct KnownHost { | |
39 | @@ -434,7 +436,9 @@ impl KnownHost { | |
40 | fn host_matches(&self, host: &str) -> bool { | |
41 | let mut match_found = false; | |
42 | let host = host.to_lowercase(); | |
43 | - // FIXME: support hashed hostnames | |
44 | + if let Some(hashed) = self.patterns.strip_prefix(HASH_HOSTNAME_PREFIX) { | |
45 | + return hashed_hostname_matches(&host, hashed); | |
46 | + } | |
47 | for pattern in self.patterns.split(',') { | |
48 | let pattern = pattern.to_lowercase(); | |
49 | // FIXME: support * and ? wildcards | |
50 | @@ -450,6 +454,16 @@ impl KnownHost { | |
51 | } | |
52 | } | |
53 | ||
54 | +fn hashed_hostname_matches(host: &str, hashed: &str) -> bool { | |
55 | + let Some((b64_salt, b64_host)) = hashed.split_once('|') else { return false; }; | |
56 | + let Ok(salt) = base64::decode(b64_salt) else { return false; }; | |
57 | + let Ok(hashed_host) = base64::decode(b64_host) else { return false; }; | |
58 | + let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt) else { return false; }; | |
59 | + mac.update(host.as_bytes()); | |
60 | + let result = mac.finalize().into_bytes(); | |
61 | + hashed_host == &result[..] | |
62 | +} | |
63 | + | |
64 | /// Loads an OpenSSH known_hosts file. | |
65 | fn load_hostfile(path: &Path) -> Result<Vec<KnownHost>, anyhow::Error> { | |
66 | let contents = cargo_util::paths::read(path)?; | |
67 | @@ -474,7 +488,7 @@ fn load_hostfile_contents(path: &Path, c | |
68 | fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option<KnownHost> { | |
69 | let line = line.trim(); | |
70 | // FIXME: @revoked and @cert-authority is currently not supported. | |
71 | - if line.is_empty() || line.starts_with(['#', '@', '|']) { | |
72 | + if line.is_empty() || line.starts_with(['#', '@']) { | |
73 | return None; | |
74 | } | |
75 | let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty()); | |
76 | @@ -506,8 +520,7 @@ mod tests { | |
77 | @revoked * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKtQsi+KPYispwm2rkMidQf30fG1Niy8XNkvASfePoca eric@host | |
78 | example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWkjI6XT2SZh3xNk5NhisA3o3sGzWR+VAKMSqHtI0aY eric@host | |
79 | 192.168.42.12 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKVYJpa0yUGaNk0NXQTPWa0tHjqRpx+7hl2diReH6DtR eric@host | |
80 | - # Hash not yet supported. | |
81 | - |1|7CMSYgzdwruFLRhwowMtKx0maIE=|Tlff1GFqc3Ao+fUWxMEVG8mJiyk= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host | |
82 | + |1|QxzZoTXIWLhUsuHAXjuDMIV3FjQ=|M6NCOIkjiWdCWqkh5+Q+/uFLGjs= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIHgN3O21U4LWtP5OzjTzPnUnSDmCNDvyvlaj6Hi65JC eric@host | |
83 | # Negation isn't terribly useful without globs. | |
84 | neg.example.com,!neg.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOXfUnaAHTlo1Qi//rNk26OcmHikmkns1Z6WW/UuuS3K eric@host | |
85 | "#; | |
86 | @@ -516,7 +529,7 @@ mod tests { | |
87 | fn known_hosts_parse() { | |
88 | let kh_path = Path::new("/home/abc/.known_hosts"); | |
89 | let khs = load_hostfile_contents(kh_path, COMMON_CONTENTS); | |
90 | - assert_eq!(khs.len(), 9); | |
91 | + assert_eq!(khs.len(), 10); | |
92 | match &khs[0].location { | |
93 | KnownHostLocation::File { path, lineno } => { | |
94 | assert_eq!(path, kh_path); | |
95 | @@ -551,7 +564,9 @@ mod tests { | |
96 | assert!(!khs[0].host_matches("example.net")); | |
97 | assert!(khs[2].host_matches("[example.net]:2222")); | |
98 | assert!(!khs[2].host_matches("example.net")); | |
99 | - assert!(!khs[8].host_matches("neg.example.com")); | |
100 | + assert!(khs[8].host_matches("hashed.example.com")); | |
101 | + assert!(!khs[8].host_matches("example.com")); | |
102 | + assert!(!khs[9].host_matches("neg.example.com")); | |
103 | } | |
104 | ||
105 | #[test] | |
106 | --- rust-cargo-0.66.0.orig/Cargo.toml | |
107 | +++ rust-cargo-0.66.0/Cargo.toml | |
108 | @@ -83,6 +86,9 @@ version = "0.3.0" | |
109 | [dependencies.hex] | |
110 | version = "0.4" | |
111 | ||
112 | +[dependencies.hmac] | |
113 | +version = "0.12.1" | |
114 | + | |
115 | [dependencies.home] | |
116 | version = "0.5" | |
117 | ||
118 | @@ -163,6 +169,9 @@ version = "0.1.0" | |
119 | version = "1.0.30" | |
120 | features = ["raw_value"] | |
121 | ||
122 | +[dependencies.sha1] | |
123 | +version = "0.10.5" | |
124 | + | |
125 | [dependencies.shell-escape] | |
126 | version = "0.1.4" | |
127 |
0 | This patch eliminates let-else usage in the code introduced | |
1 | to fix CVE-2022-46176 as that construct is not stabalised in | |
2 | the version of rustc currently in Debian. | |
3 | ||
4 | It was written specifical for Debian by Peter Michael Green. | |
5 | ||
6 | Index: cargo/src/cargo/sources/git/known_hosts.rs | |
7 | =================================================================== | |
8 | --- cargo.orig/src/cargo/sources/git/known_hosts.rs | |
9 | +++ cargo/src/cargo/sources/git/known_hosts.rs | |
10 | @@ -89,11 +89,13 @@ pub fn certificate_check( | |
11 | config_known_hosts: Option<&Vec<Value<String>>>, | |
12 | diagnostic_home_config: &str, | |
13 | ) -> Result<CertificateCheckStatus, git2::Error> { | |
14 | - let Some(host_key) = cert.as_hostkey() else { | |
15 | + let host_key = cert.as_hostkey(); | |
16 | + if host_key.is_none() { | |
17 | // Return passthrough for TLS X509 certificates to use whatever validation | |
18 | // was done in git2. | |
19 | return Ok(CertificateCheckStatus::CertificatePassthrough) | |
20 | }; | |
21 | + let host_key = host_key.unwrap(); | |
22 | // If a nonstandard port is in use, check for that first. | |
23 | // The fallback to check without a port is handled in the HostKeyNotFound handler. | |
24 | let host_maybe_port = match port { | |
25 | @@ -234,9 +236,11 @@ fn check_ssh_known_hosts( | |
26 | host: &str, | |
27 | config_known_hosts: Option<&Vec<Value<String>>>, | |
28 | ) -> Result<(), KnownHostError> { | |
29 | - let Some(remote_host_key) = cert_host_key.hostkey() else { | |
30 | + let remote_host_key = cert_host_key.hostkey(); | |
31 | + if remote_host_key.is_none() { | |
32 | return Err(anyhow::format_err!("remote host key is not available").into()); | |
33 | }; | |
34 | + let remote_host_key = remote_host_key.unwrap(); | |
35 | let remote_key_type = cert_host_key.hostkey_type().unwrap(); | |
36 | ||
37 | // Collect all the known host entries from disk. | |
38 | @@ -455,10 +459,18 @@ impl KnownHost { | |
39 | } | |
40 | ||
41 | fn hashed_hostname_matches(host: &str, hashed: &str) -> bool { | |
42 | - let Some((b64_salt, b64_host)) = hashed.split_once('|') else { return false; }; | |
43 | - let Ok(salt) = base64::decode(b64_salt) else { return false; }; | |
44 | - let Ok(hashed_host) = base64::decode(b64_host) else { return false; }; | |
45 | - let Ok(mut mac) = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt) else { return false; }; | |
46 | + let hostandsalt = hashed.split_once('|'); | |
47 | + if hostandsalt.is_none() { return false; }; | |
48 | + let (b64_salt, b64_host) = hostandsalt.unwrap(); | |
49 | + let salt = base64::decode(b64_salt); | |
50 | + if salt.is_err() { return false; }; | |
51 | + let salt = salt.unwrap(); | |
52 | + let hashed_host = base64::decode(b64_host); | |
53 | + if hashed_host.is_err() { return false; }; | |
54 | + let hashed_host = hashed_host.unwrap(); | |
55 | + let mac = hmac::Hmac::<sha1::Sha1>::new_from_slice(&salt); | |
56 | + if mac.is_err() { return false; }; | |
57 | + let mut mac = mac.unwrap(); | |
58 | mac.update(host.as_bytes()); | |
59 | let result = mac.finalize().into_bytes(); | |
60 | hashed_host == &result[..] |
0 | 0 | disable-vendor.patch |
1 | CVE-2022-46176-01-validate-ssh-host.keys.patch | |
2 | CVE-2022-46176-02-add-support-for-deserializing-vec-value-string.patch | |
3 | CVE-2022-46176-03-support-configuring-ssh-known-hosts.patch | |
4 | CVE-2022-46176-04-add-some-known-hosts-tests-and-fix-comma-bug.patch | |
5 | CVE-2022-46176-05-remove-let-else.patch | |
6 | CVE-2022-46176-06-add-test-for-config-value-in-toml-array.patch | |
7 | CVE-2022-46176-07-support-hashed-hostnames.patch | |
8 | CVE-2022-46176-08-eliminate-let-else.patch |