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