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