]> git.proxmox.com Git - rustc.git/blame - src/tools/cargo/tests/testsuite/ssh.rs
New upstream version 1.74.1+dfsg1
[rustc.git] / src / tools / cargo / tests / testsuite / ssh.rs
CommitLineData
0a29b90c
FG
1//! Network tests for SSH connections.
2//!
3//! Note that these tests will generally require setting CARGO_CONTAINER_TESTS
4//! or CARGO_PUBLIC_NETWORK_TESTS.
5//!
6//! NOTE: The container tests almost certainly won't work on Windows.
7
8use cargo_test_support::containers::{Container, ContainerHandle, MkFile};
9use cargo_test_support::git::cargo_uses_gitoxide;
10use cargo_test_support::{paths, process, project, Project};
11use std::fs;
12use std::io::Write;
13use std::path::PathBuf;
14
15fn ssh_repo_url(container: &ContainerHandle, name: &str) -> String {
16 let port = container.port_mappings[&22];
17 format!("ssh://testuser@127.0.0.1:{port}/repos/{name}.git")
18}
19
20/// The path to the client's private key.
21fn key_path() -> PathBuf {
22 paths::home().join(".ssh/id_ed25519")
23}
24
25/// Generates the SSH keys for authenticating into the container.
26fn gen_ssh_keys() -> String {
27 let path = key_path();
28 process("ssh-keygen")
29 .args(&["-t", "ed25519", "-N", "", "-f"])
30 .arg(&path)
31 .exec_with_output()
32 .unwrap();
33 let pub_key = path.with_extension("pub");
34 fs::read_to_string(pub_key).unwrap()
35}
36
37/// Handler for running ssh-agent for SSH authentication.
38///
39/// Be sure to set `SSH_AUTH_SOCK` when running a process in order to use the
40/// agent. Keys will need to be copied into the container with the
41/// `authorized_keys()` method.
42struct Agent {
43 sock: PathBuf,
44 pid: String,
45 ssh_dir: PathBuf,
46 pub_key: String,
47}
48
49impl Agent {
50 fn launch() -> Agent {
51 let ssh_dir = paths::home().join(".ssh");
52 fs::create_dir(&ssh_dir).unwrap();
53 let pub_key = gen_ssh_keys();
54
55 let sock = paths::root().join("agent");
56 let output = process("ssh-agent")
57 .args(&["-s", "-a"])
58 .arg(&sock)
59 .exec_with_output()
60 .unwrap();
61 let stdout = std::str::from_utf8(&output.stdout).unwrap();
62 let start = stdout.find("SSH_AGENT_PID=").unwrap() + 14;
63 let end = &stdout[start..].find(';').unwrap();
64 let pid = (&stdout[start..start + end]).to_string();
65 eprintln!("SSH_AGENT_PID={pid}");
66 process("ssh-add")
67 .arg(key_path())
68 .env("SSH_AUTH_SOCK", &sock)
69 .exec_with_output()
70 .unwrap();
71 Agent {
72 sock,
73 pid,
74 ssh_dir,
75 pub_key,
76 }
77 }
78
79 /// Returns a `MkFile` which can be passed into the `Container` builder to
80 /// copy an `authorized_keys` file containing this agent's public key.
81 fn authorized_keys(&self) -> MkFile {
82 MkFile::path("home/testuser/.ssh/authorized_keys")
83 .contents(self.pub_key.as_bytes())
84 .mode(0o600)
85 .uid(100)
86 .gid(101)
87 }
88}
89
90impl Drop for Agent {
91 fn drop(&mut self) {
92 if let Err(e) = process("ssh-agent")
93 .args(&["-k", "-a"])
94 .arg(&self.sock)
95 .env("SSH_AGENT_PID", &self.pid)
96 .exec_with_output()
97 {
98 eprintln!("failed to stop ssh-agent: {e:?}");
99 }
100 }
101}
102
103/// Common project used for several tests.
104fn foo_bar_project(url: &str) -> Project {
105 project()
106 .file(
107 "Cargo.toml",
108 &format!(
109 r#"
110 [package]
111 name = "foo"
112 version = "0.1.0"
113
114 [dependencies]
115 bar = {{ git = "{url}" }}
116 "#
117 ),
118 )
119 .file("src/lib.rs", "")
120 .build()
121}
122
123#[cargo_test(container_test)]
124fn no_known_host() {
125 // When host is not known, it should show an error.
126 let sshd = Container::new("sshd").launch();
127 let url = ssh_repo_url(&sshd, "bar");
128 let p = foo_bar_project(&url);
129 p.cargo("fetch")
130 .with_status(101)
131 .with_stderr(
132 "\
133[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`
134error: failed to get `bar` as a dependency of package `foo v0.1.0 ([ROOT]/foo)`
135
136Caused by:
137 failed to load source for dependency `bar`
138
139Caused by:
140 Unable to update ssh://testuser@127.0.0.1:[..]/repos/bar.git
141
142Caused by:
143 failed to clone into: [ROOT]/home/.cargo/git/db/bar-[..]
144
145Caused by:
146 error: unknown SSH host key
147 The SSH host key for `[127.0.0.1]:[..]` is not known and cannot be validated.
148
149 To resolve this issue, add the host key to the `net.ssh.known-hosts` array in \
150 your Cargo configuration (such as [ROOT]/home/.cargo/config.toml) or in your \
151 OpenSSH known_hosts file at [ROOT]/home/.ssh/known_hosts
152
153 The key to add is:
154
155 [127.0.0.1]:[..] ecdsa-sha2-nistp256 AAAA[..]
156
157 The ECDSA key fingerprint is: SHA256:[..]
158 This fingerprint should be validated with the server administrator that it is correct.
159
160 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
161 for more information.
162",
163 )
164 .run();
165}
166
167#[cargo_test(container_test)]
168fn known_host_works() {
169 // The key displayed in the error message should work when added to known_hosts.
170 let agent = Agent::launch();
171 let sshd = Container::new("sshd")
172 .file(agent.authorized_keys())
173 .launch();
174 let url = ssh_repo_url(&sshd, "bar");
175 let p = foo_bar_project(&url);
176 let output = p
177 .cargo("fetch")
178 .env("SSH_AUTH_SOCK", &agent.sock)
179 .build_command()
180 .output()
181 .unwrap();
182 let stderr = std::str::from_utf8(&output.stderr).unwrap();
183
184 // Validate the fingerprint while we're here.
185 let fingerprint = stderr
186 .lines()
781aab86 187 .find_map(|line| line.strip_prefix(" The ECDSA key fingerprint is: "))
0a29b90c
FG
188 .unwrap()
189 .trim();
0a29b90c
FG
190 let finger_out = sshd.exec(&["ssh-keygen", "-l", "-f", "/etc/ssh/ssh_host_ecdsa_key.pub"]);
191 let gen_finger = std::str::from_utf8(&finger_out.stdout).unwrap();
192 // <key-size> <fingerprint> <comments…>
193 let gen_finger = gen_finger.split_whitespace().nth(1).unwrap();
194 assert_eq!(fingerprint, gen_finger);
195
196 // Add the key to known_hosts, and try again.
197 let key = stderr
198 .lines()
199 .find(|line| line.starts_with(" [127.0.0.1]:"))
200 .unwrap()
201 .trim();
202 fs::write(agent.ssh_dir.join("known_hosts"), key).unwrap();
203 p.cargo("fetch")
204 .env("SSH_AUTH_SOCK", &agent.sock)
205 .with_stderr("[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`")
206 .run();
207}
208
209#[cargo_test(container_test)]
210fn same_key_different_hostname() {
211 // The error message should mention if an identical key was found.
212 let agent = Agent::launch();
213 let sshd = Container::new("sshd").launch();
214
215 let hostkey = sshd.read_file("/etc/ssh/ssh_host_ecdsa_key.pub");
216 let known_hosts = format!("example.com {hostkey}");
217 fs::write(agent.ssh_dir.join("known_hosts"), known_hosts).unwrap();
218
219 let url = ssh_repo_url(&sshd, "bar");
220 let p = foo_bar_project(&url);
221 p.cargo("fetch")
222 .with_status(101)
223 .with_stderr(
224 "\
225[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`
226error: failed to get `bar` as a dependency of package `foo v0.1.0 ([ROOT]/foo)`
227
228Caused by:
229 failed to load source for dependency `bar`
230
231Caused by:
232 Unable to update ssh://testuser@127.0.0.1:[..]/repos/bar.git
233
234Caused by:
235 failed to clone into: [ROOT]/home/.cargo/git/db/bar-[..]
236
237Caused by:
238 error: unknown SSH host key
239 The SSH host key for `[127.0.0.1]:[..]` is not known and cannot be validated.
240
241 To resolve this issue, add the host key to the `net.ssh.known-hosts` array in \
242 your Cargo configuration (such as [ROOT]/home/.cargo/config.toml) or in your \
243 OpenSSH known_hosts file at [ROOT]/home/.ssh/known_hosts
244
245 The key to add is:
246
247 [127.0.0.1]:[..] ecdsa-sha2-nistp256 AAAA[..]
248
249 The ECDSA key fingerprint is: SHA256:[..]
250 This fingerprint should be validated with the server administrator that it is correct.
251 Note: This host key was found, but is associated with a different host:
252 [ROOT]/home/.ssh/known_hosts line 1: example.com
253
254 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts \
255 for more information.
256",
257 )
258 .run();
259}
260
261#[cargo_test(container_test)]
262fn known_host_without_port() {
263 // A known_host entry without a port should match a connection to a non-standard port.
264 let agent = Agent::launch();
265 let sshd = Container::new("sshd")
266 .file(agent.authorized_keys())
267 .launch();
268
269 let hostkey = sshd.read_file("/etc/ssh/ssh_host_ecdsa_key.pub");
270 // The important part of this test is that this line does not have a port.
271 let known_hosts = format!("127.0.0.1 {hostkey}");
272 fs::write(agent.ssh_dir.join("known_hosts"), known_hosts).unwrap();
273 let url = ssh_repo_url(&sshd, "bar");
274 let p = foo_bar_project(&url);
275 p.cargo("fetch")
276 .env("SSH_AUTH_SOCK", &agent.sock)
277 .with_stderr("[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`")
278 .run();
279}
280
281#[cargo_test(container_test)]
282fn hostname_case_insensitive() {
283 // hostname checking should be case-insensitive.
284 let agent = Agent::launch();
285 let sshd = Container::new("sshd")
286 .file(agent.authorized_keys())
287 .launch();
288
289 // Consider using `gethostname-rs` instead?
290 let hostname = process("hostname").exec_with_output().unwrap();
291 let hostname = std::str::from_utf8(&hostname.stdout).unwrap().trim();
292 let inv_hostname = if hostname.chars().any(|c| c.is_lowercase()) {
293 hostname.to_uppercase()
294 } else {
295 // There should be *some* chars in the name.
296 assert!(hostname.chars().any(|c| c.is_uppercase()));
297 hostname.to_lowercase()
298 };
299 eprintln!("converted {hostname} to {inv_hostname}");
300
301 let hostkey = sshd.read_file("/etc/ssh/ssh_host_ecdsa_key.pub");
302 let known_hosts = format!("{inv_hostname} {hostkey}");
303 fs::write(agent.ssh_dir.join("known_hosts"), known_hosts).unwrap();
304 let port = sshd.port_mappings[&22];
305 let url = format!("ssh://testuser@{hostname}:{port}/repos/bar.git");
306 let p = foo_bar_project(&url);
307 p.cargo("fetch")
308 .env("SSH_AUTH_SOCK", &agent.sock)
309 .with_stderr(&format!(
310 "[UPDATING] git repository `ssh://testuser@{hostname}:{port}/repos/bar.git`"
311 ))
312 .run();
313}
314
315#[cargo_test(container_test)]
316fn invalid_key_error() {
317 // An error when a known_host value doesn't match.
318 let agent = Agent::launch();
319 let sshd = Container::new("sshd")
320 .file(agent.authorized_keys())
321 .launch();
322
323 let port = sshd.port_mappings[&22];
324 let known_hosts = format!(
325 "[127.0.0.1]:{port} ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLqLMclVr7MDuaVsm3sEnnq2OrGxTFiHSw90wd6N14BU8xVC9cZldC3rJ58Wmw6bEVKPjk7foNG0lHwS5bCKX+U=\n"
326 );
327 fs::write(agent.ssh_dir.join("known_hosts"), known_hosts).unwrap();
328 let url = ssh_repo_url(&sshd, "bar");
329 let p = foo_bar_project(&url);
330 p.cargo("fetch")
331 .env("SSH_AUTH_SOCK", &agent.sock)
332 .with_status(101)
333 .with_stderr(&format!("\
334[UPDATING] git repository `ssh://testuser@127.0.0.1:{port}/repos/bar.git`
335error: failed to get `bar` as a dependency of package `foo v0.1.0 ([ROOT]/foo)`
336
337Caused by:
338 failed to load source for dependency `bar`
339
340Caused by:
341 Unable to update ssh://testuser@127.0.0.1:{port}/repos/bar.git
342
343Caused by:
344 failed to clone into: [ROOT]/home/.cargo/git/db/bar-[..]
345
346Caused by:
347 error: SSH host key has changed for `[127.0.0.1]:{port}`
348 *********************************
349 * WARNING: HOST KEY HAS CHANGED *
350 *********************************
351 This may be caused by a man-in-the-middle attack, or the server may have changed its host key.
352
353 The ECDSA fingerprint for the key from the remote host is:
354 SHA256:[..]
355
356 You are strongly encouraged to contact the server administrator for `[127.0.0.1]:{port}` \
357 to verify that this new key is correct.
358
359 If you can verify that the server has a new key, you can resolve this error by \
360 removing the old ecdsa-sha2-nistp256 key for `[127.0.0.1]:{port}` located at \
361 [ROOT]/home/.ssh/known_hosts line 1, and adding the new key to the \
362 `net.ssh.known-hosts` array in your Cargo configuration (such as \
363 [ROOT]/home/.cargo/config.toml) or in your OpenSSH known_hosts file at \
364 [ROOT]/home/.ssh/known_hosts
365
366 The key provided by the remote host is:
367
368 [127.0.0.1]:{port} ecdsa-sha2-nistp256 [..]
369
370 See https://doc.rust-lang.org/stable/cargo/appendix/git-authentication.html#ssh-known-hosts for more information.
371"))
372 .run();
373 // Add the key, it should work even with the old key left behind.
374 let hostkey = sshd.read_file("/etc/ssh/ssh_host_ecdsa_key.pub");
375 let known_hosts_path = agent.ssh_dir.join("known_hosts");
376 let mut f = fs::OpenOptions::new()
377 .append(true)
378 .open(known_hosts_path)
379 .unwrap();
380 write!(f, "[127.0.0.1]:{port} {hostkey}").unwrap();
381 drop(f);
382 p.cargo("fetch")
383 .env("SSH_AUTH_SOCK", &agent.sock)
384 .with_stderr("[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`")
385 .run();
386}
387
388// For unknown reasons, this test occasionally fails on Windows with a
389// LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE error:
390// failed to start SSH session: Unable to exchange encryption keys; class=Ssh (23)
391#[cargo_test(public_network_test, ignore_windows = "test is flaky on windows")]
392fn invalid_github_key() {
393 // A key for github.com in known_hosts should override the built-in key.
394 // This uses a bogus key which should result in an error.
395 let ssh_dir = paths::home().join(".ssh");
396 fs::create_dir(&ssh_dir).unwrap();
397 let known_hosts = "\
398 github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLqLMclVr7MDuaVsm3sEnnq2OrGxTFiHSw90wd6N14BU8xVC9cZldC3rJ58Wmw6bEVKPjk7foNG0lHwS5bCKX+U=\n\
399 github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDgi+8rMcyFCBq5y7BXrb2aaYGhMjlU3QDy7YDvtNL5KSecYOsaqQHaXr87Bbx0EEkgbhK4kVMkmThlCoNITQS9Vc3zIMQ+Tg6+O4qXx719uCzywl50Tb5tDqPGMj54jcq3VUiu/dvse0yeehyvzoPNWewgGWLx11KI4A4wOwMnc6guhculEWe9DjGEjUQ34lPbmdfu/Hza7ZVu/RhgF/wc43uzXWB2KpMEqtuY1SgRlCZqTASoEtfKZi0AuM7AEdOwE5aTotS4CQZHWimb1bMFpF4DAq92CZ8Jhrm4rWETbO29WmjviCJEA3KNQyd3oA7H9AE9z/22PJaVEmjiZZ+wyLgwyIpOlsnHYNEdGeQMQ4SgLRkARLwcnKmByv1AAxsBW4LI3Os4FpwxVPdXHcBebydtvxIsbtUVkkq99nbsIlnSRFSTvb0alrdzRuKTdWpHtN1v9hagFqmeCx/kJfH76NXYBbtaWZhSOnxfEbhLYuOb+IS4jYzHAIkzy9FjVuk=\n\
400 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEeMB6BUAW6FfvfLxRO3kGASe0yXnrRT4kpqncsup2b2\n";
401 fs::write(ssh_dir.join("known_hosts"), known_hosts).unwrap();
402 let p = project()
403 .file(
404 "Cargo.toml",
405 r#"
406 [package]
407 name = "foo"
408 version = "0.1.0"
409
410 [dependencies]
411 bitflags = { git = "ssh://git@github.com/rust-lang/bitflags.git", tag = "1.3.2" }
412 "#,
413 )
414 .file("src/lib.rs", "")
415 .build();
416 p.cargo("fetch")
417 .with_status(101)
418 .with_stderr_contains(if cargo_uses_gitoxide() {
419 " git@github.com: Permission denied (publickey)."
420 } else {
421 " error: SSH host key has changed for `github.com`"
422 })
423 .run();
424}
425
426// For unknown reasons, this test occasionally fails on Windows with a
427// LIBSSH2_ERROR_KEY_EXCHANGE_FAILURE error:
428// failed to start SSH session: Unable to exchange encryption keys; class=Ssh (23)
429#[cargo_test(public_network_test, ignore_windows = "test is flaky on windows")]
430fn bundled_github_works() {
431 // The bundled key for github.com works.
432 //
433 // Use a bogus auth sock to force an authentication error.
434 // On Windows, if the agent service is running, it could allow a
435 // successful authentication.
436 //
437 // If the bundled hostkey did not work, it would result in an "unknown SSH
438 // host key" instead.
439 let bogus_auth_sock = paths::home().join("ssh_auth_sock");
440 let p = project()
441 .file(
442 "Cargo.toml",
443 r#"
444 [package]
445 name = "foo"
446 version = "0.1.0"
447
448 [dependencies]
449 bitflags = { git = "ssh://git@github.com/rust-lang/bitflags.git", tag = "1.3.2" }
450 "#,
451 )
452 .file("src/lib.rs", "")
453 .build();
454 let shared_stderr = "\
455[UPDATING] git repository `ssh://git@github.com/rust-lang/bitflags.git`
456error: failed to get `bitflags` as a dependency of package `foo v0.1.0 ([ROOT]/foo)`
457
458Caused by:
459 failed to load source for dependency `bitflags`
460
461Caused by:
462 Unable to update ssh://git@github.com/rust-lang/bitflags.git?tag=1.3.2
463
464Caused by:
465 failed to clone into: [ROOT]/home/.cargo/git/db/bitflags-[..]
466
467Caused by:
468 failed to authenticate when downloading repository
469
470 *";
471 let expected = if cargo_uses_gitoxide() {
472 format!(
473 "{shared_stderr} attempted to find username/password via `credential.helper`, but maybe the found credentials were incorrect
474
475 if the git CLI succeeds then `net.git-fetch-with-cli` may help here
476 https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli
477
478Caused by:
479 Credentials provided for \"ssh://git@github.com/rust-lang/bitflags.git\" were not accepted by the remote
480
481Caused by:
482 git@github.com: Permission denied (publickey).
483"
484 )
485 } else {
486 format!(
487 "{shared_stderr} attempted ssh-agent authentication, but no usernames succeeded: `git`
488
489 if the git CLI succeeds then `net.git-fetch-with-cli` may help here
490 https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli
491
492Caused by:
493 no authentication methods succeeded
494"
495 )
496 };
497 p.cargo("fetch")
498 .env("SSH_AUTH_SOCK", &bogus_auth_sock)
499 .with_status(101)
500 .with_stderr(&expected)
501 .run();
502
503 let shared_stderr = "\
504[UPDATING] git repository `ssh://git@github.com:22/rust-lang/bitflags.git`
505error: failed to get `bitflags` as a dependency of package `foo v0.1.0 ([ROOT]/foo)`
506
507Caused by:
508 failed to load source for dependency `bitflags`
509
510Caused by:
511 Unable to update ssh://git@github.com:22/rust-lang/bitflags.git?tag=1.3.2
512
513Caused by:
514 failed to clone into: [ROOT]/home/.cargo/git/db/bitflags-[..]
515
516Caused by:
517 failed to authenticate when downloading repository
518
519 *";
520
521 let expected = if cargo_uses_gitoxide() {
522 format!(
523 "{shared_stderr} attempted to find username/password via `credential.helper`, but maybe the found credentials were incorrect
524
525 if the git CLI succeeds then `net.git-fetch-with-cli` may help here
526 https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli
527
528Caused by:
529 Credentials provided for \"ssh://git@github.com:22/rust-lang/bitflags.git\" were not accepted by the remote
530
531Caused by:
532 git@github.com: Permission denied (publickey).
533"
534 )
535 } else {
536 format!(
537 "{shared_stderr} attempted ssh-agent authentication, but no usernames succeeded: `git`
538
539 if the git CLI succeeds then `net.git-fetch-with-cli` may help here
540 https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli
541
542Caused by:
543 no authentication methods succeeded
544"
545 )
546 };
547
548 // Explicit :22 should also work with bundled.
549 p.change_file(
550 "Cargo.toml",
551 r#"
552 [package]
553 name = "foo"
554 version = "0.1.0"
555
556 [dependencies]
557 bitflags = { git = "ssh://git@github.com:22/rust-lang/bitflags.git", tag = "1.3.2" }
558 "#,
559 );
560 p.cargo("fetch")
561 .env("SSH_AUTH_SOCK", &bogus_auth_sock)
562 .with_status(101)
563 .with_stderr(&expected)
564 .run();
565}
566
567#[cargo_test(container_test)]
568fn ssh_key_in_config() {
569 // known_host in config works.
570 let agent = Agent::launch();
571 let sshd = Container::new("sshd")
572 .file(agent.authorized_keys())
573 .launch();
574 let hostkey = sshd.read_file("/etc/ssh/ssh_host_ecdsa_key.pub");
575 let url = ssh_repo_url(&sshd, "bar");
576 let p = foo_bar_project(&url);
577 p.change_file(
578 ".cargo/config.toml",
579 &format!(
580 r#"
581 [net.ssh]
582 known-hosts = ['127.0.0.1 {}']
583 "#,
584 hostkey.trim()
585 ),
586 );
587 p.cargo("fetch")
588 .env("SSH_AUTH_SOCK", &agent.sock)
589 .with_stderr("[UPDATING] git repository `ssh://testuser@127.0.0.1:[..]/repos/bar.git`")
590 .run();
591}