View Javadoc
1   /*
2    * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.junit.ssh;
11  
12  import static java.nio.charset.StandardCharsets.UTF_8;
13  import static org.junit.Assert.assertArrayEquals;
14  import static org.junit.Assert.assertEquals;
15  import static org.junit.Assert.assertFalse;
16  import static org.junit.Assert.assertNotNull;
17  import static org.junit.Assert.assertThrows;
18  import static org.junit.Assert.assertTrue;
19  import static org.junit.Assume.assumeTrue;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.concurrent.TimeUnit;
27  
28  import org.eclipse.jgit.api.errors.TransportException;
29  import org.eclipse.jgit.errors.CommandFailedException;
30  import org.eclipse.jgit.transport.CredentialItem;
31  import org.eclipse.jgit.transport.URIish;
32  import org.eclipse.jgit.util.FS;
33  import org.eclipse.jgit.util.SshSupport;
34  import org.junit.Test;
35  import org.junit.experimental.theories.DataPoints;
36  import org.junit.experimental.theories.Theory;
37  
38  /**
39   * The ssh tests. Concrete subclasses can re-use these tests by implementing the
40   * abstract operations from {@link SshTestHarness}. This gives a way to test
41   * different ssh clients against a unified test suite.
42   */
43  public abstract class SshTestBase extends SshTestHarness {
44  
45  	@DataPoints
46  	public static String[] KEY_RESOURCES = { //
47  			"id_dsa", //
48  			"id_rsa_1024", //
49  			"id_rsa_2048", //
50  			"id_rsa_3072", //
51  			"id_rsa_4096", //
52  			"id_ecdsa_256", //
53  			"id_ecdsa_384", //
54  			"id_ecdsa_521", //
55  			"id_ed25519", //
56  			// And now encrypted. Passphrase is "testpass".
57  			"id_dsa_testpass", //
58  			"id_rsa_1024_testpass", //
59  			"id_rsa_2048_testpass", //
60  			"id_rsa_3072_testpass", //
61  			"id_rsa_4096_testpass", //
62  			"id_ecdsa_256_testpass", //
63  			"id_ecdsa_384_testpass", //
64  			"id_ecdsa_521_testpass", //
65  			"id_ed25519_testpass", //
66  			"id_ed25519_expensive_testpass" };
67  
68  	protected File defaultCloneDir;
69  
70  	@Override
71  	public void setUp() throws Exception {
72  		super.setUp();
73  		defaultCloneDir = new File(getTemporaryDirectory(), "cloned");
74  	}
75  
76  	@Test
77  	public void testSshWithoutConfig() throws Exception {
78  		assertThrows(TransportException.class,
79  				() -> cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
80  						+ "/doesntmatter", defaultCloneDir, null));
81  	}
82  
83  	@Test
84  	public void testSingleCommand() throws Exception {
85  		installConfig("IdentityFile " + privateKey1.getAbsolutePath());
86  		String command = SshTestGitServer.ECHO_COMMAND + " 1 without timeout";
87  		long start = System.nanoTime();
88  		String reply = SshSupport.runSshCommand(
89  				new URIish("ssh://" + TEST_USER + "@localhost:" + testPort),
90  				null, FS.DETECTED, command, 0); // 0 == no timeout
91  		long elapsed = System.nanoTime() - start;
92  		assertEquals(command, reply);
93  		// Now that we have an idea how long this takes on the test
94  		// infrastructure, try again with a timeout.
95  		command = SshTestGitServer.ECHO_COMMAND + " 1 expecting no timeout";
96  		// Still use a generous timeout.
97  		int timeout = 10 * ((int) TimeUnit.NANOSECONDS.toSeconds(elapsed) + 1);
98  		reply = SshSupport.runSshCommand(
99  				new URIish("ssh://" + TEST_USER + "@localhost:" + testPort),
100 				null, FS.DETECTED, command, timeout);
101 		assertEquals(command, reply);
102 	}
103 
104 	@Test
105 	public void testSingleCommandWithTimeoutExpired() throws Exception {
106 		installConfig("IdentityFile " + privateKey1.getAbsolutePath());
107 		String command = SshTestGitServer.ECHO_COMMAND + " 2 EXPECTING TIMEOUT";
108 
109 		CommandFailedException e = assertThrows(CommandFailedException.class,
110 				() -> SshSupport.runSshCommand(new URIish(
111 						"ssh://" + TEST_USER + "@localhost:" + testPort), null,
112 						FS.DETECTED, command, 1));
113 		assertTrue(e.getMessage().contains(command));
114 		assertTrue(e.getMessage().contains("time"));
115 	}
116 
117 	@Test
118 	public void testSshWithGlobalIdentity() throws Exception {
119 		cloneWith(
120 				"ssh://" + TEST_USER + "@localhost:" + testPort
121 						+ "/doesntmatter",
122 				defaultCloneDir, null,
123 				"IdentityFile " + privateKey1.getAbsolutePath());
124 	}
125 
126 	@Test
127 	public void testSshWithDefaultIdentity() throws Exception {
128 		File idRsa = new File(privateKey1.getParentFile(), "id_rsa");
129 		Files.copy(privateKey1.toPath(), idRsa.toPath());
130 		// We expect the session factory to pick up these keys...
131 		cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
132 				+ "/doesntmatter", defaultCloneDir, null);
133 	}
134 
135 	@Test
136 	public void testSshWithConfig() throws Exception {
137 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
138 				"Host localhost", //
139 				"HostName localhost", //
140 				"Port " + testPort, //
141 				"User " + TEST_USER, //
142 				"IdentityFile " + privateKey1.getAbsolutePath());
143 	}
144 
145 	@Test
146 	public void testSshWithConfigEncryptedUnusedKey() throws Exception {
147 		// Copy the encrypted test key from the bundle.
148 		File encryptedKey = new File(sshDir, "id_dsa");
149 		copyTestResource("id_dsa_testpass", encryptedKey);
150 		TestCredentialsProvider provider = new TestCredentialsProvider(
151 				"testpass");
152 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
153 				"Host localhost", //
154 				"HostName localhost", //
155 				"Port " + testPort, //
156 				"User " + TEST_USER, //
157 				"IdentityFile " + privateKey1.getAbsolutePath());
158 		assertEquals("CredentialsProvider should not have been called", 0,
159 				provider.getLog().size());
160 	}
161 
162 	@Test
163 	public void testSshWithConfigEncryptedUnusedKeyInConfigLast()
164 			throws Exception {
165 		// Copy the encrypted test key from the bundle.
166 		File encryptedKey = new File(sshDir, "id_dsa_test_key");
167 		copyTestResource("id_dsa_testpass", encryptedKey);
168 		TestCredentialsProvider provider = new TestCredentialsProvider(
169 				"testpass");
170 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
171 				"Host localhost", //
172 				"HostName localhost", //
173 				"Port " + testPort, //
174 				"User " + TEST_USER, //
175 				"IdentityFile " + privateKey1.getAbsolutePath(),
176 				"IdentityFile " + encryptedKey.getAbsolutePath());
177 		// This test passes with JSch per chance because JSch completely ignores
178 		// the second IdentityFile
179 		assertEquals("CredentialsProvider should not have been called", 0,
180 				provider.getLog().size());
181 	}
182 
183 	private boolean isJsch() {
184 		return getSessionFactory().getType().equals("jsch");
185 	}
186 
187 	@Test
188 	public void testSshWithConfigEncryptedUnusedKeyInConfigFirst()
189 			throws Exception {
190 		// Test cannot pass with JSch; it handles only one IdentityFile.
191 		// assumeTrue(!(getSessionFactory() instanceof
192 		// JschConfigSessionFactory)); gives in bazel a failure with "Never
193 		// found parameters that satisfied method assumptions."
194 		// In maven it's fine!?
195 		if (isJsch()) {
196 			return;
197 		}
198 		// Copy the encrypted test key from the bundle.
199 		File encryptedKey = new File(sshDir, "id_dsa_test_key");
200 		copyTestResource("id_dsa_testpass", encryptedKey);
201 		TestCredentialsProvider provider = new TestCredentialsProvider(
202 				"testpass");
203 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
204 				"Host localhost", //
205 				"HostName localhost", //
206 				"Port " + testPort, //
207 				"User " + TEST_USER, //
208 				"IdentityFile " + encryptedKey.getAbsolutePath(),
209 				"IdentityFile " + privateKey1.getAbsolutePath());
210 		assertEquals("CredentialsProvider should have been called once", 1,
211 				provider.getLog().size());
212 	}
213 
214 	@Test
215 	public void testSshEncryptedUsedKeyCached() throws Exception {
216 		// Make sure we are asked for the password only once if we do several
217 		// operations with an encrypted key.
218 		File encryptedKey = new File(sshDir, "id_dsa_test_key");
219 		copyTestResource("id_dsa_testpass", encryptedKey);
220 		File encryptedPublicKey = new File(sshDir, "id_dsa_test_key.pub");
221 		copyTestResource("id_dsa_testpass.pub", encryptedPublicKey);
222 		server.setTestUserPublicKey(encryptedPublicKey.toPath());
223 		TestCredentialsProvider provider = new TestCredentialsProvider(
224 				"testpass");
225 		pushTo(provider,
226 				cloneWith("ssh://localhost/doesntmatter", //
227 						defaultCloneDir, provider, //
228 						"Host localhost", //
229 						"HostName localhost", //
230 						"Port " + testPort, //
231 						"User " + TEST_USER, //
232 						"IdentityFile " + encryptedKey.getAbsolutePath()));
233 		assertEquals("CredentialsProvider should have been called once", 1,
234 				provider.getLog().size());
235 	}
236 
237 	@Test(expected = TransportException.class)
238 	public void testSshEncryptedUsedKeyWrongPassword() throws Exception {
239 		File encryptedKey = new File(sshDir, "id_dsa_test_key");
240 		copyTestResource("id_dsa_testpass", encryptedKey);
241 		File encryptedPublicKey = new File(sshDir, "id_dsa_test_key.pub");
242 		copyTestResource("id_dsa_testpass.pub", encryptedPublicKey);
243 		server.setTestUserPublicKey(encryptedPublicKey.toPath());
244 		TestCredentialsProvider provider = new TestCredentialsProvider(
245 				"wrongpass");
246 		cloneWith("ssh://localhost/doesntmatter", //
247 				defaultCloneDir, provider, //
248 				"Host localhost", //
249 				"HostName localhost", //
250 				"Port " + testPort, //
251 				"User " + TEST_USER, //
252 				"NumberOfPasswordPrompts 1", //
253 				"IdentityFile " + encryptedKey.getAbsolutePath());
254 	}
255 
256 	@Test
257 	public void testSshEncryptedUsedKeySeveralPassword() throws Exception {
258 		File encryptedKey = new File(sshDir, "id_dsa_test_key");
259 		copyTestResource("id_dsa_testpass", encryptedKey);
260 		File encryptedPublicKey = new File(sshDir, "id_dsa_test_key.pub");
261 		copyTestResource("id_dsa_testpass.pub", encryptedPublicKey);
262 		server.setTestUserPublicKey(encryptedPublicKey.toPath());
263 		TestCredentialsProvider provider = new TestCredentialsProvider(
264 				"wrongpass", "wrongpass2", "testpass");
265 		cloneWith("ssh://localhost/doesntmatter", //
266 				defaultCloneDir, provider, //
267 				"Host localhost", //
268 				"HostName localhost", //
269 				"Port " + testPort, //
270 				"User " + TEST_USER, //
271 				"IdentityFile " + encryptedKey.getAbsolutePath());
272 		assertEquals("CredentialsProvider should have been called 3 times", 3,
273 				provider.getLog().size());
274 	}
275 
276 	@Test(expected = TransportException.class)
277 	public void testSshWithoutKnownHosts() throws Exception {
278 		assertTrue("Could not delete known_hosts", knownHosts.delete());
279 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
280 				"Host localhost", //
281 				"HostName localhost", //
282 				"Port " + testPort, //
283 				"User " + TEST_USER, //
284 				"IdentityFile " + privateKey1.getAbsolutePath());
285 	}
286 
287 	@Test
288 	public void testSshWithoutKnownHostsWithProviderAsk()
289 			throws Exception {
290 		File copiedHosts = new File(knownHosts.getParentFile(),
291 				"copiedKnownHosts");
292 		assertTrue("Failed to rename known_hosts",
293 				knownHosts.renameTo(copiedHosts));
294 		// The provider will answer "yes" to all questions, so we should be able
295 		// to connect and end up with a new known_hosts file with the host key.
296 		TestCredentialsProvider provider = new TestCredentialsProvider();
297 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
298 				"Host localhost", //
299 				"HostName localhost", //
300 				"Port " + testPort, //
301 				"User " + TEST_USER, //
302 				"IdentityFile " + privateKey1.getAbsolutePath());
303 		List<LogEntry> messages = provider.getLog();
304 		assertFalse("Expected user interaction", messages.isEmpty());
305 		if (isJsch()) {
306 			// JSch doesn't create a non-existing file.
307 			assertEquals("Expected to be asked about the key", 1,
308 					messages.size());
309 			return;
310 		}
311 		assertEquals(
312 				"Expected to be asked about the key, and the file creation",
313 				2, messages.size());
314 		assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists());
315 		// Instead of checking the file contents, let's just clone again
316 		// without provider. If it works, the server host key was written
317 		// correctly.
318 		File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
319 		cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
320 				"Host localhost", //
321 				"HostName localhost", //
322 				"Port " + testPort, //
323 				"User " + TEST_USER, //
324 				"IdentityFile " + privateKey1.getAbsolutePath());
325 	}
326 
327 	@Test
328 	public void testSshWithoutKnownHostsWithProviderAcceptNew()
329 			throws Exception {
330 		File copiedHosts = new File(knownHosts.getParentFile(),
331 				"copiedKnownHosts");
332 		assertTrue("Failed to rename known_hosts",
333 				knownHosts.renameTo(copiedHosts));
334 		TestCredentialsProvider provider = new TestCredentialsProvider();
335 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
336 				"Host localhost", //
337 				"HostName localhost", //
338 				"Port " + testPort, //
339 				"User " + TEST_USER, //
340 				"StrictHostKeyChecking accept-new", //
341 				"IdentityFile " + privateKey1.getAbsolutePath());
342 		if (isJsch()) {
343 			// JSch doesn't create new files.
344 			assertTrue("CredentialsProvider not called",
345 					provider.getLog().isEmpty());
346 			return;
347 		}
348 		assertEquals("Expected to be asked about the file creation", 1,
349 				provider.getLog().size());
350 		assertTrue("~/.ssh/known_hosts should exist now", knownHosts.exists());
351 		// Instead of checking the file contents, let's just clone again
352 		// without provider. If it works, the server host key was written
353 		// correctly.
354 		File clonedAgain = new File(getTemporaryDirectory(), "cloned2");
355 		cloneWith("ssh://localhost/doesntmatter", clonedAgain, null, //
356 				"Host localhost", //
357 				"HostName localhost", //
358 				"Port " + testPort, //
359 				"User " + TEST_USER, //
360 				"IdentityFile " + privateKey1.getAbsolutePath());
361 	}
362 
363 	@Test(expected = TransportException.class)
364 	public void testSshWithoutKnownHostsDeny() throws Exception {
365 		File copiedHosts = new File(knownHosts.getParentFile(),
366 				"copiedKnownHosts");
367 		assertTrue("Failed to rename known_hosts",
368 				knownHosts.renameTo(copiedHosts));
369 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
370 				"Host localhost", //
371 				"HostName localhost", //
372 				"Port " + testPort, //
373 				"User " + TEST_USER, //
374 				"StrictHostKeyChecking yes", //
375 				"IdentityFile " + privateKey1.getAbsolutePath());
376 	}
377 
378 	@Test(expected = TransportException.class)
379 	public void testSshModifiedHostKeyDeny()
380 			throws Exception {
381 		File copiedHosts = new File(knownHosts.getParentFile(),
382 				"copiedKnownHosts");
383 		assertTrue("Failed to rename known_hosts",
384 				knownHosts.renameTo(copiedHosts));
385 		// Now produce a new known_hosts file containing some other key.
386 		createKnownHostsFile(knownHosts, "localhost", testPort, publicKey1);
387 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
388 				"Host localhost", //
389 				"HostName localhost", //
390 				"Port " + testPort, //
391 				"User " + TEST_USER, //
392 				"StrictHostKeyChecking yes", //
393 				"IdentityFile " + privateKey1.getAbsolutePath());
394 	}
395 
396 	@Test(expected = TransportException.class)
397 	public void testSshModifiedHostKeyWithProviderDeny() throws Exception {
398 		File copiedHosts = new File(knownHosts.getParentFile(),
399 				"copiedKnownHosts");
400 		assertTrue("Failed to rename known_hosts",
401 				knownHosts.renameTo(copiedHosts));
402 		// Now produce a new known_hosts file containing some other key.
403 		createKnownHostsFile(knownHosts, "localhost", testPort, publicKey1);
404 		TestCredentialsProvider provider = new TestCredentialsProvider();
405 		try {
406 			cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
407 					"Host localhost", //
408 					"HostName localhost", //
409 					"Port " + testPort, //
410 					"User " + TEST_USER, //
411 					"StrictHostKeyChecking yes", //
412 					"IdentityFile " + privateKey1.getAbsolutePath());
413 		} catch (Exception e) {
414 			assertEquals("Expected to be told about the modified key", 1,
415 					provider.getLog().size());
416 			assertTrue("Only messages expected", provider.getLog().stream()
417 					.flatMap(l -> l.getItems().stream()).allMatch(
418 							c -> c instanceof CredentialItem.InformationalMessage));
419 			throw e;
420 		}
421 	}
422 
423 	private void checkKnownHostsModifiedHostKey(File backup, File newFile,
424 			String wrongKey) throws IOException {
425 		List<String> oldLines = Files.readAllLines(backup.toPath(), UTF_8);
426 		// Find the original entry. We should have that again in known_hosts.
427 		String oldKeyPart = null;
428 		for (String oldLine : oldLines) {
429 			if (oldLine.contains("[localhost]:")) {
430 				String[] parts = oldLine.split("\\s+");
431 				if (parts.length > 2) {
432 					oldKeyPart = parts[parts.length - 2] + ' '
433 							+ parts[parts.length - 1];
434 					break;
435 				}
436 			}
437 		}
438 		assertNotNull("Old key not found", oldKeyPart);
439 		List<String> newLines = Files.readAllLines(newFile.toPath(), UTF_8);
440 		assertFalse("Old host key still found in known_hosts file" + newFile,
441 				hasHostKey("localhost", testPort, wrongKey, newLines));
442 		assertTrue("New host key not found in known_hosts file" + newFile,
443 				hasHostKey("localhost", testPort, oldKeyPart, newLines));
444 
445 	}
446 
447 	@Test
448 	public void testSshModifiedHostKeyAllow() throws Exception {
449 		assertTrue("Failed to delete known_hosts", knownHosts.delete());
450 		createKnownHostsFile(knownHosts, "localhost", testPort, publicKey1);
451 		File backup = new File(getTemporaryDirectory(), "backupKnownHosts");
452 		Files.copy(knownHosts.toPath(), backup.toPath());
453 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
454 				"Host localhost", //
455 				"HostName localhost", //
456 				"Port " + testPort, //
457 				"User " + TEST_USER, //
458 				"StrictHostKeyChecking no", //
459 				"IdentityFile " + privateKey1.getAbsolutePath());
460 		// File should not have been updated!
461 		String[] oldLines = Files
462 				.readAllLines(backup.toPath(), UTF_8)
463 				.toArray(new String[0]);
464 		String[] newLines = Files
465 				.readAllLines(knownHosts.toPath(), UTF_8)
466 				.toArray(new String[0]);
467 		assertArrayEquals("Known hosts file should not be modified", oldLines,
468 				newLines);
469 	}
470 
471 	@Test
472 	public void testSshModifiedHostKeyAsk() throws Exception {
473 		File copiedHosts = new File(knownHosts.getParentFile(),
474 				"copiedKnownHosts");
475 		assertTrue("Failed to rename known_hosts",
476 				knownHosts.renameTo(copiedHosts));
477 		String wrongKeyPart = createKnownHostsFile(knownHosts, "localhost",
478 				testPort, publicKey1);
479 		TestCredentialsProvider provider = new TestCredentialsProvider();
480 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, provider, //
481 				"Host localhost", //
482 				"HostName localhost", //
483 				"Port " + testPort, //
484 				"User " + TEST_USER, //
485 				"IdentityFile " + privateKey1.getAbsolutePath());
486 		checkKnownHostsModifiedHostKey(copiedHosts, knownHosts, wrongKeyPart);
487 		assertEquals("Expected to be asked about the modified key", 1,
488 				provider.getLog().size());
489 	}
490 
491 	@Test
492 	public void testSshCloneWithConfigAndPush() throws Exception {
493 		pushTo(cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
494 				"Host localhost", //
495 				"HostName localhost", //
496 				"Port " + testPort, //
497 				"User " + TEST_USER, //
498 				"IdentityFile " + privateKey1.getAbsolutePath()));
499 	}
500 
501 	@Test
502 	public void testSftpWithConfig() throws Exception {
503 		cloneWith("sftp://localhost/.git", defaultCloneDir, null, //
504 				"Host localhost", //
505 				"HostName localhost", //
506 				"Port " + testPort, //
507 				"User " + TEST_USER, //
508 				"IdentityFile " + privateKey1.getAbsolutePath());
509 	}
510 
511 	@Test
512 	public void testSftpCloneWithConfigAndPush() throws Exception {
513 		pushTo(cloneWith("sftp://localhost/.git", defaultCloneDir, null, //
514 				"Host localhost", //
515 				"HostName localhost", //
516 				"Port " + testPort, //
517 				"User " + TEST_USER, //
518 				"IdentityFile " + privateKey1.getAbsolutePath()));
519 	}
520 
521 	@Test(expected = TransportException.class)
522 	public void testSshWithConfigWrongKey() throws Exception {
523 		cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
524 				"Host localhost", //
525 				"HostName localhost", //
526 				"Port " + testPort, //
527 				"User " + TEST_USER, //
528 				"IdentityFile " + privateKey2.getAbsolutePath());
529 	}
530 
531 	@Test
532 	public void testSshWithWrongUserNameInConfig() throws Exception {
533 		// Bug 526778
534 		cloneWith(
535 				"ssh://" + TEST_USER + "@localhost:" + testPort
536 						+ "/doesntmatter",
537 				defaultCloneDir, null, //
538 				"Host localhost", //
539 				"HostName localhost", //
540 				"User sombody_else", //
541 				"IdentityFile " + privateKey1.getAbsolutePath());
542 	}
543 
544 	@Test
545 	public void testSshWithWrongPortInConfig() throws Exception {
546 		// Bug 526778
547 		cloneWith(
548 				"ssh://" + TEST_USER + "@localhost:" + testPort
549 						+ "/doesntmatter",
550 				defaultCloneDir, null, //
551 				"Host localhost", //
552 				"HostName localhost", //
553 				"Port 22", //
554 				"User " + TEST_USER, //
555 				"IdentityFile " + privateKey1.getAbsolutePath());
556 	}
557 
558 	@Test
559 	public void testSshWithAliasInConfig() throws Exception {
560 		// Bug 531118
561 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
562 				"Host git", //
563 				"HostName localhost", //
564 				"Port " + testPort, //
565 				"User " + TEST_USER, //
566 				"IdentityFile " + privateKey1.getAbsolutePath(), "", //
567 				"Host localhost", //
568 				"HostName localhost", //
569 				"Port 22", //
570 				"User someone_else", //
571 				"IdentityFile " + privateKey2.getAbsolutePath());
572 	}
573 
574 	@Test
575 	public void testSshWithUnknownCiphersInConfig() throws Exception {
576 		// Bug 535672
577 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
578 				"Host git", //
579 				"HostName localhost", //
580 				"Port " + testPort, //
581 				"User " + TEST_USER, //
582 				"IdentityFile " + privateKey1.getAbsolutePath(), //
583 				"Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr");
584 	}
585 
586 	@Test
587 	public void testSshWithUnknownHostKeyAlgorithmsInConfig()
588 			throws Exception {
589 		// Bug 535672
590 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
591 				"Host git", //
592 				"HostName localhost", //
593 				"Port " + testPort, //
594 				"User " + TEST_USER, //
595 				"IdentityFile " + privateKey1.getAbsolutePath(), //
596 				"HostKeyAlgorithms foobar,ssh-rsa,ssh-dss");
597 	}
598 
599 	@Test
600 	public void testSshWithUnknownKexAlgorithmsInConfig()
601 			throws Exception {
602 		// Bug 535672
603 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
604 				"Host git", //
605 				"HostName localhost", //
606 				"Port " + testPort, //
607 				"User " + TEST_USER, //
608 				"IdentityFile " + privateKey1.getAbsolutePath(), //
609 				"KexAlgorithms foobar,diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521");
610 	}
611 
612 	@Test
613 	public void testSshWithMinimalHostKeyAlgorithmsInConfig()
614 			throws Exception {
615 		// Bug 537790
616 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
617 				"Host git", //
618 				"HostName localhost", //
619 				"Port " + testPort, //
620 				"User " + TEST_USER, //
621 				"IdentityFile " + privateKey1.getAbsolutePath(), //
622 				"HostKeyAlgorithms ssh-rsa,ssh-dss");
623 	}
624 
625 	@Test
626 	public void testSshWithUnknownAuthInConfig() throws Exception {
627 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
628 				"Host git", //
629 				"HostName localhost", //
630 				"Port " + testPort, //
631 				"User " + TEST_USER, //
632 				"IdentityFile " + privateKey1.getAbsolutePath(), //
633 				"PreferredAuthentications gssapi-with-mic,hostbased,publickey,keyboard-interactive,password");
634 	}
635 
636 	@Test(expected = TransportException.class)
637 	public void testSshWithNoMatchingAuthInConfig() throws Exception {
638 		// Server doesn't do password, and anyway we set no password.
639 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
640 				"Host git", //
641 				"HostName localhost", //
642 				"Port " + testPort, //
643 				"User " + TEST_USER, //
644 				"IdentityFile " + privateKey1.getAbsolutePath(), //
645 				"PreferredAuthentications password");
646 	}
647 
648 	@Test
649 	public void testRsaHostKeySecond() throws Exception {
650 		// See https://git.eclipse.org/r/#/c/130402/ : server has EcDSA
651 		// (preferred), RSA, we have RSA in known_hosts: client and server
652 		// should agree on RSA.
653 		File newHostKey = new File(getTemporaryDirectory(), "newhostkey");
654 		copyTestResource("id_ecdsa_256", newHostKey);
655 		server.addHostKey(newHostKey.toPath(), true);
656 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
657 				"Host git", //
658 				"HostName localhost", //
659 				"Port " + testPort, //
660 				"User " + TEST_USER, //
661 				"IdentityFile " + privateKey1.getAbsolutePath());
662 	}
663 
664 	@Test
665 	public void testEcDsaHostKey() throws Exception {
666 		// See https://git.eclipse.org/r/#/c/130402/ : server has RSA
667 		// (preferred), EcDSA, we have EcDSA in known_hosts: client and server
668 		// should agree on EcDSA.
669 		File newHostKey = new File(getTemporaryDirectory(), "newhostkey");
670 		copyTestResource("id_ecdsa_256", newHostKey);
671 		server.addHostKey(newHostKey.toPath(), false);
672 		File newHostKeyPub = new File(getTemporaryDirectory(),
673 				"newhostkey.pub");
674 		copyTestResource("id_ecdsa_256.pub", newHostKeyPub);
675 		createKnownHostsFile(knownHosts, "localhost", testPort, newHostKeyPub);
676 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, null, //
677 				"Host git", //
678 				"HostName localhost", //
679 				"Port " + testPort, //
680 				"User " + TEST_USER, //
681 				"IdentityFile " + privateKey1.getAbsolutePath());
682 	}
683 
684 	@Test
685 	public void testPasswordAuth() throws Exception {
686 		server.enablePasswordAuthentication();
687 		TestCredentialsProvider provider = new TestCredentialsProvider(
688 				TEST_USER.toUpperCase(Locale.ROOT));
689 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
690 				"Host git", //
691 				"HostName localhost", //
692 				"Port " + testPort, //
693 				"User " + TEST_USER, //
694 				"PreferredAuthentications password");
695 	}
696 
697 	@Test
698 	public void testPasswordAuthSeveralTimes() throws Exception {
699 		server.enablePasswordAuthentication();
700 		TestCredentialsProvider provider = new TestCredentialsProvider(
701 				"wrongpass", "wrongpass", TEST_USER.toUpperCase(Locale.ROOT));
702 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
703 				"Host git", //
704 				"HostName localhost", //
705 				"Port " + testPort, //
706 				"User " + TEST_USER, //
707 				"PreferredAuthentications password");
708 	}
709 
710 	@Test(expected = TransportException.class)
711 	public void testPasswordAuthWrongPassword() throws Exception {
712 		server.enablePasswordAuthentication();
713 		TestCredentialsProvider provider = new TestCredentialsProvider(
714 				"wrongpass");
715 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
716 				"Host git", //
717 				"HostName localhost", //
718 				"Port " + testPort, //
719 				"User " + TEST_USER, //
720 				"PreferredAuthentications password");
721 	}
722 
723 	@Test(expected = TransportException.class)
724 	public void testPasswordAuthNoPassword() throws Exception {
725 		server.enablePasswordAuthentication();
726 		TestCredentialsProvider provider = new TestCredentialsProvider();
727 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
728 				"Host git", //
729 				"HostName localhost", //
730 				"Port " + testPort, //
731 				"User " + TEST_USER, //
732 				"PreferredAuthentications password");
733 	}
734 
735 	@Test(expected = TransportException.class)
736 	public void testPasswordAuthCorrectPasswordTooLate() throws Exception {
737 		server.enablePasswordAuthentication();
738 		TestCredentialsProvider provider = new TestCredentialsProvider(
739 				"wrongpass", "wrongpass", "wrongpass",
740 				TEST_USER.toUpperCase(Locale.ROOT));
741 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
742 				"Host git", //
743 				"HostName localhost", //
744 				"Port " + testPort, //
745 				"User " + TEST_USER, //
746 				"PreferredAuthentications password");
747 	}
748 
749 	@Test
750 	public void testKeyboardInteractiveAuth() throws Exception {
751 		server.enableKeyboardInteractiveAuthentication();
752 		TestCredentialsProvider provider = new TestCredentialsProvider(
753 				TEST_USER.toUpperCase(Locale.ROOT));
754 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
755 				"Host git", //
756 				"HostName localhost", //
757 				"Port " + testPort, //
758 				"User " + TEST_USER, //
759 				"PreferredAuthentications keyboard-interactive");
760 	}
761 
762 	@Test
763 	public void testKeyboardInteractiveAuthSeveralTimes() throws Exception {
764 		server.enableKeyboardInteractiveAuthentication();
765 		TestCredentialsProvider provider = new TestCredentialsProvider(
766 				"wrongpass", "wrongpass", TEST_USER.toUpperCase(Locale.ROOT));
767 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
768 				"Host git", //
769 				"HostName localhost", //
770 				"Port " + testPort, //
771 				"User " + TEST_USER, //
772 				"PreferredAuthentications keyboard-interactive");
773 	}
774 
775 	@Test(expected = TransportException.class)
776 	public void testKeyboardInteractiveAuthWrongPassword() throws Exception {
777 		server.enableKeyboardInteractiveAuthentication();
778 		TestCredentialsProvider provider = new TestCredentialsProvider(
779 				"wrongpass");
780 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
781 				"Host git", //
782 				"HostName localhost", //
783 				"Port " + testPort, //
784 				"User " + TEST_USER, //
785 				"PreferredAuthentications keyboard-interactive");
786 	}
787 
788 	@Test(expected = TransportException.class)
789 	public void testKeyboardInteractiveAuthNoPassword() throws Exception {
790 		server.enableKeyboardInteractiveAuthentication();
791 		TestCredentialsProvider provider = new TestCredentialsProvider();
792 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
793 				"Host git", //
794 				"HostName localhost", //
795 				"Port " + testPort, //
796 				"User " + TEST_USER, //
797 				"PreferredAuthentications keyboard-interactive");
798 	}
799 
800 	@Test(expected = TransportException.class)
801 	public void testKeyboardInteractiveAuthCorrectPasswordTooLate()
802 			throws Exception {
803 		server.enableKeyboardInteractiveAuthentication();
804 		TestCredentialsProvider provider = new TestCredentialsProvider(
805 				"wrongpass", "wrongpass", "wrongpass",
806 				TEST_USER.toUpperCase(Locale.ROOT));
807 		cloneWith("ssh://git/doesntmatter", defaultCloneDir, provider, //
808 				"Host git", //
809 				"HostName localhost", //
810 				"Port " + testPort, //
811 				"User " + TEST_USER, //
812 				"PreferredAuthentications keyboard-interactive");
813 	}
814 
815 	@Theory
816 	public void testSshKeys(String keyName) throws Exception {
817 		// JSch fails on ECDSA 384/521 keys. Compare
818 		// https://sourceforge.net/p/jsch/patches/10/
819 		assumeTrue(!(isJsch() && (keyName.contains("ed25519")
820 				|| keyName.startsWith("id_ecdsa_384")
821 				|| keyName.startsWith("id_ecdsa_521"))));
822 		File cloned = new File(getTemporaryDirectory(), "cloned");
823 		String keyFileName = keyName + "_key";
824 		File privateKey = new File(sshDir, keyFileName);
825 		copyTestResource(keyName, privateKey);
826 		File publicKey = new File(sshDir, keyFileName + ".pub");
827 		copyTestResource(keyName + ".pub", publicKey);
828 		server.setTestUserPublicKey(publicKey.toPath());
829 		TestCredentialsProvider provider = new TestCredentialsProvider(
830 				"testpass");
831 		pushTo(provider,
832 				cloneWith("ssh://localhost/doesntmatter", //
833 						cloned, provider, //
834 						"Host localhost", //
835 						"HostName localhost", //
836 						"Port " + testPort, //
837 						"User " + TEST_USER, //
838 						"IdentityFile " + privateKey.getAbsolutePath()));
839 		int expectedCalls = keyName.endsWith("testpass") ? 1 : 0;
840 		assertEquals("Unexpected calls to CredentialsProvider", expectedCalls,
841 				provider.getLog().size());
842 		// Should now also work without credentials provider, even if the key
843 		// was encrypted.
844 		cloned = new File(getTemporaryDirectory(), "cloned2");
845 		pushTo(null,
846 				cloneWith("ssh://localhost/doesntmatter", //
847 						cloned, null, //
848 						"Host localhost", //
849 						"HostName localhost", //
850 						"Port " + testPort, //
851 						"User " + TEST_USER, //
852 						"IdentityFile " + privateKey.getAbsolutePath()));
853 	}
854 }