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