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