Ssh tests with an Apache MINA sshd test git server

Add a simple ssh git server based on Apache MINA sshd, and use it
in new tests that verify ssh operations and in particular a number
of bugs that had cropped up over time in JSch.

The git server supports fetching only, and sftp access.

The tests are all in an abstract base class; the concrete JschSshTest
class only provides ssh-specific test setup. So the same tests could
be run easily also with some other ssh client.

Bug: 520927
Change-Id: Ide6687b717fb497a29fc83f22b07390a26dfce1d
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/WORKSPACE b/WORKSPACE
index d327e13..30fbfd6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -53,6 +53,18 @@
 )
 
 maven_jar(
+    name = "sshd-core",
+    artifact = "org.apache.sshd:sshd-core:2.0.0",
+    sha1 = "f4275079a2463cfd2bf1548a80e1683288a8e86b",
+)
+
+maven_jar(
+    name = "sshd-sftp",
+    artifact = "org.apache.sshd:sshd-sftp:2.0.0",
+    sha1 = "a12d64dc2d5d23271a4dc58075e55f9c64a68494",
+)
+
+maven_jar(
     name = "commons-codec",
     artifact = "commons-codec:commons-codec:1.10",
     sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
diff --git a/lib/BUILD b/lib/BUILD
index 7cd420a..35cb79e 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -59,6 +59,24 @@
 )
 
 java_library(
+    name = "sshd-core",
+    visibility = [
+        "//org.eclipse.jgit.junit:__pkg__",
+        "//org.eclipse.jgit.test:__pkg__",
+    ],
+    exports = ["@sshd-core//jar"],
+)
+
+java_library(
+    name = "sshd-sftp",
+    visibility = [
+        "//org.eclipse.jgit.junit:__pkg__",
+        "//org.eclipse.jgit.test:__pkg__",
+    ],
+    exports = ["@sshd-sftp//jar"],
+)
+
+java_library(
     name = "javaewah",
     visibility = ["//visibility:public"],
     exports = ["@javaewah//jar"],
diff --git a/org.eclipse.jgit.junit/BUILD b/org.eclipse.jgit.junit/BUILD
index 74498fd..cba2318 100644
--- a/org.eclipse.jgit.junit/BUILD
+++ b/org.eclipse.jgit.junit/BUILD
@@ -8,6 +8,8 @@
     resources = glob(["resources/**"]),
     deps = [
         "//lib:junit",
+        "//lib:sshd-core",
+        "//lib:sshd-sftp",
         # We want these deps to be provided_deps
         "//org.eclipse.jgit:jgit",
     ],
diff --git a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
index 9721c42..e44ee03 100644
--- a/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.junit/META-INF/MANIFEST.MF
@@ -8,7 +8,21 @@
 Bundle-Vendor: %provider_name
 Bundle-ActivationPolicy: lazy
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.api;version="[5.2.0,5.3.0)",
+Import-Package:  org.apache.sshd.common;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.config.keys;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.file.virtualfs;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.helpers;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.kex;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.keyprovider;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.session;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.util.logging;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.util.security;version="[2.0.0,2.1.0)",
+ org.apache.sshd.server;version="[2.0.0,2.1.0)",
+ org.apache.sshd.server.command;version="[2.0.0,2.1.0)",
+ org.apache.sshd.server.shell;version="[2.0.0,2.1.0)",
+ org.apache.sshd.server.subsystem.sftp;version="[2.0.0,2.1.0)",
+ org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)",
+ org.eclipse.jgit.api;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.api.errors;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.dircache;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.errors;version="[5.2.0,5.3.0)",
@@ -18,6 +32,7 @@
  org.eclipse.jgit.merge;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.revwalk;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.storage.file;version="[5.2.0,5.3.0)",
+ org.eclipse.jgit.transport;version="5.2.0",
  org.eclipse.jgit.treewalk;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.treewalk.filter;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.util;version="[5.2.0,5.3.0)",
@@ -26,7 +41,8 @@
  org.junit;version="[4.12,5.0.0)",
  org.junit.rules;version="[4.12,5.0.0)",
  org.junit.runner;version="[4.12,5.0.0)",
- org.junit.runners.model;version="[4.12,5.0.0)"
+ org.junit.runners.model;version="[4.12,5.0.0)",
+ org.slf4j;version="[1.7.0,2.0.0)"
 Export-Package: org.eclipse.jgit.junit;version="5.2.0";
   uses:="org.eclipse.jgit.dircache,
    org.eclipse.jgit.lib,
@@ -35,5 +51,10 @@
    org.eclipse.jgit.treewalk,
    org.eclipse.jgit.util,
    org.eclipse.jgit.storage.file,
-   org.eclipse.jgit.api",
- org.eclipse.jgit.junit.time;version="5.2.0"
+   org.eclipse.jgit.api,
+   org.junit.rules,
+   org.junit.runners.model,
+   org.junit.runner,
+   org.eclipse.jgit.util.time",
+ org.eclipse.jgit.junit.ssh;version="5.2.0",
+ org.eclipse.jgit.junit.time;version="5.2.0";uses:="org.eclipse.jgit.util.time"
diff --git a/org.eclipse.jgit.junit/pom.xml b/org.eclipse.jgit.junit/pom.xml
index 24e2c71..112c73f 100644
--- a/org.eclipse.jgit.junit/pom.xml
+++ b/org.eclipse.jgit.junit/pom.xml
@@ -74,6 +74,18 @@
     </dependency>
 
     <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-core</artifactId>
+      <version>${apache-sshd-version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-sftp</artifactId>
+      <version>${apache-sshd-version}</version>
+    </dependency>
+
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>provided</scope>
diff --git a/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java
new file mode 100644
index 0000000..675a115
--- /dev/null
+++ b/org.eclipse.jgit.junit/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.junit.ssh;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.text.MessageFormat;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.apache.sshd.common.config.keys.IdentityUtils;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.command.AbstractCommandSupport;
+import org.apache.sshd.server.shell.UnknownCommand;
+import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.UploadPack;
+
+/**
+ * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd.
+ * <p>
+ * Supports only a single repository. Authenticates only the given test user
+ * against his given test public key. ssh is limited to fetching (upload-pack).
+ * </p>
+ *
+ * @since 5.2
+ */
+public class SshTestGitServer {
+
+	@NonNull
+	private String testUser;
+
+	@NonNull
+	private PublicKey testKey;
+
+	@NonNull
+	private Repository repository;
+
+	private final ExecutorService executorService = Executors
+			.newFixedThreadPool(2);
+
+	private final SshServer server;
+
+	/**
+	 * Creates a ssh git <em>test</em> server. It serves one single repository,
+	 * and accepts public-key authentication for exactly one test user.
+	 *
+	 * @param testUser
+	 *            user name of the test user
+	 * @param testKey
+	 *            <em>private</em> key file of the test user; the server will
+	 *            only user the public key from it
+	 * @param repository
+	 *            to serve
+	 * @param hostKey
+	 *            the unencrypted private key to use as host key
+	 * @throws IOException
+	 * @throws GeneralSecurityException
+	 */
+	public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
+			@NonNull Repository repository, @NonNull byte[] hostKey)
+			throws IOException, GeneralSecurityException {
+		this.testUser = testUser;
+		this.testKey = IdentityUtils
+				.loadIdentities(Collections.singletonMap("A", testKey), null)
+				.get("A").getPublic();
+		this.repository = repository;
+		server = SshServer.setUpDefaultServer();
+		// Set host key
+		server.setKeyPairProvider(new KeyPairProvider() {
+
+			@Override
+			public Iterable<KeyPair> loadKeys() {
+				try (ByteArrayInputStream in = new ByteArrayInputStream(
+						hostKey)) {
+					return Collections.singletonList(
+							SecurityUtils.loadKeyPairIdentity("", in, null));
+				} catch (IOException | GeneralSecurityException e) {
+					return null;
+				}
+			}
+
+		});
+		// SFTP.
+		server.setFileSystemFactory(new VirtualFileSystemFactory() {
+
+			@Override
+			protected Path computeRootDir(Session session) throws IOException {
+				return SshTestGitServer.this.repository.getDirectory()
+						.getParentFile().getAbsoluteFile().toPath();
+			}
+		});
+		server.setSubsystemFactories(Collections
+				.singletonList((new SftpSubsystemFactory.Builder()).build()));
+		// No shell
+		server.setShellFactory(null);
+		// Disable some authentications
+		server.setPasswordAuthenticator(null);
+		server.setKeyboardInteractiveAuthenticator(null);
+		server.setGSSAuthenticator(null);
+		server.setHostBasedAuthenticator(null);
+		// Accept only the test user/public key
+		server.setPublickeyAuthenticator((userName, publicKey, session) -> {
+			return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
+					.compareKeys(SshTestGitServer.this.testKey, publicKey);
+		});
+		server.setCommandFactory(command -> {
+			if (command.startsWith("git-upload-pack")
+					|| command.startsWith("git upload-pack")) {
+				return new GitUploadPackCommand(command, executorService);
+			}
+			return new UnknownCommand(command);
+		});
+	}
+
+	/**
+	 * Starts the test server, listening on a random port.
+	 *
+	 * @return the port the server listens on; test clients should connect to
+	 *         that port
+	 * @throws IOException
+	 */
+	public int start() throws IOException {
+		server.start();
+		return server.getPort();
+	}
+
+	/**
+	 * Stops the test server.
+	 *
+	 * @throws IOException
+	 */
+	public void stop() throws IOException {
+		executorService.shutdownNow();
+		server.stop(true);
+	}
+
+	private class GitUploadPackCommand extends AbstractCommandSupport {
+
+		protected GitUploadPackCommand(String command,
+				ExecutorService executorService) {
+			super(command, executorService, false);
+		}
+
+		@Override
+		public void run() {
+			UploadPack uploadPack = new UploadPack(repository);
+			String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
+			if (gitProtocol != null) {
+				uploadPack
+						.setExtraParameters(Collections.singleton(gitProtocol));
+			}
+			try {
+				uploadPack.upload(getInputStream(), getOutputStream(),
+						getErrorStream());
+				onExit(0);
+			} catch (IOException e) {
+				log.warn(
+						MessageFormat.format("Could not run {0}", getCommand()),
+						e);
+				onExit(-1, e.toString());
+			}
+		}
+
+	}
+}
diff --git a/org.eclipse.jgit.test/BUILD b/org.eclipse.jgit.test/BUILD
index 186de25..07597f3 100644
--- a/org.eclipse.jgit.test/BUILD
+++ b/org.eclipse.jgit.test/BUILD
@@ -20,6 +20,7 @@
     "revwalk/RevWalkTestCase.java",
     "transport/ObjectIdMatcher.java",
     "transport/SpiTransport.java",
+    "transport/SshTestBase.java",
     "treewalk/FileTreeIteratorWithTimeControl.java",
     "treewalk/filter/AlwaysCloneTreeFilter.java",
     "test/resources/SampleDataRepositoryTestCase.java",
@@ -44,6 +45,9 @@
     resources = DATA,
     deps = [
         "//lib:junit",
+        "//lib:jsch",
+        "//lib:sshd-core",
+        "//lib:sshd-sftp",
         "//org.eclipse.jgit:jgit",
         "//org.eclipse.jgit.junit:junit",
     ],
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 6df9e39..6514277 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -10,6 +10,7 @@
 Bundle-RequiredExecutionEnvironment: JavaSE-1.8
 Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
  com.jcraft.jsch;version="[0.1.54,0.2.0)",
+ org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.api;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.api.errors;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.attributes;version="[5.2.0,5.3.0)",
@@ -34,6 +35,7 @@
  org.eclipse.jgit.internal.storage.reftree;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.internal.transport.parser;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.junit;version="[5.2.0,5.3.0)",
+ org.eclipse.jgit.junit.ssh;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.lfs;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.lib;version="[5.2.0,5.3.0)",
  org.eclipse.jgit.merge;version="[5.2.0,5.3.0)",
diff --git a/org.eclipse.jgit.test/pom.xml b/org.eclipse.jgit.test/pom.xml
index 6aa34f5..3d5df9a 100644
--- a/org.eclipse.jgit.test/pom.xml
+++ b/org.eclipse.jgit.test/pom.xml
@@ -112,6 +112,18 @@
       <artifactId>org.eclipse.jgit.pgm</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-core</artifactId>
+      <version>${apache-sshd-version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.sshd</groupId>
+      <artifactId>sshd-sftp</artifactId>
+      <version>${apache-sshd-version}</version>
+    </dependency>
   </dependencies>
 
   <profiles>
diff --git a/org.eclipse.jgit.test/tests.bzl b/org.eclipse.jgit.test/tests.bzl
index bc06e3e..b9ad8b2 100644
--- a/org.eclipse.jgit.test/tests.bzl
+++ b/org.eclipse.jgit.test/tests.bzl
@@ -41,7 +41,13 @@
             additional_deps = [
                 "//lib:jsch",
             ]
-
+        if src.endswith("JSchSshTest.java"):
+            additional_deps = [
+                "//lib:jsch",
+                "//lib:jzlib",
+                "//lib:sshd-core",
+                "//lib:sshd-sftp",
+            ]
         heap_size = "-Xmx256m"
         if src.endswith("HugeCommitMessageTest.java"):
             heap_size = "-Xmx512m"
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test
new file mode 100644
index 0000000..cc39e8b
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test
@@ -0,0 +1,15 @@
+-----BEGIN DSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,D7B8FC3F4E304A2A22754B068767081F
+
+IewkLt6JyqtPccAsnfeLv7IMlLvgm7tqQSYK1/CLhmDE0aZXViD8sqxLA6dVjmkp
+BVyk7EBpp43PnVQYsDcMPnyM8H83vNRDtIQ6fxM1PJafiP7Rbn1k1fDh7DwA48PU
+FnT6zZ9aYEKYMto0WIdQ86j/uY+LtYygQDDoZ2ohn2NlpykeyrSp0bDRIoW6sdc5
++LlfDtq2usv3fcxMAJpO/SSN78LvBlyOK4n/JAVSkPawsW1WsIrXA52mk0iUhjYc
+aYOCuL+wA7OmHAOpfS5HUXZ4i/7qONnLBkEqeIOcgTmShh1c4oWw9TjWK1AzdSDU
+G2nkRJ/8zK/jdm5wcmrjrzuREM1VbCiXHlVoHYI0W1Z9etOgz1cj4KLz/bB8Nf+8
+shCez1Aw5ec33BzwysfwymfAKeXjYaxdKcur3j+UdXAlYRD28BRnWmTL5Jx82eUu
+NIh0U9pHkn+PjdzmjSPEUP7wzDjQQacaQTkBRf5gPyOYfv/+Mnq6YyflKaPYmkEr
+eztO22VZlpyp/hj2LzBav9wi0++teInNQGU+GxHedsWm4+YpffMhz1bz5ZUQ670A
+0WJJH3k/KnxbCY3usj4eJr+CsX+LNZhm+rKyjRDmRwA=
+-----END DSA PRIVATE KEY-----
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub
new file mode 100644
index 0000000..0528a92
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/transport/id_dsa_test.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAIsXi0EUiI6GmhHqrwwjvO2wdujW46+uXM/SG2GVI3KxCSf95B2XgXBsgiKH0sy3guyqjDcP4Ph5Mctg1IxqmqugN6xf9YB6lf09bRdIbumVGU6nXW7bZDHdk9nmvWy56vurofwvhoRnQBUJ3L4n7dxxvXhIyRPOxptayOS2ZcnRAAAAFQDsgGxVxcBBM9y0Rm3kNz/R64CYEQAAAIEAgCbyCJNZb66KQBMO7B+NPxx0caSKjZ+3TpWL6pLJGTAu1pztd1wpElECNCEBhTX9p1HEypTIjOUFU2gjgaBLUcWE0JK+/4vJjjvaENvrQardH0EeRfrazhpRY+X6ytUTk0YPDuQn+ZqBhXxAoD8BA+TJMvk7oMpMUTyr6LGBuj4AAACAeXCfOrKY6wHuMkHHpa9Ix95T+7h5ZrSosrV1WO5g9X04LNiPFRXvGyMWYF17VaGqVWID5NbbGP4PqwSw0rjmw7c/xxV2DYNfJ5NFWsDHxhI6RP9AaGTKcdIEykWEkGgJDiVF/DJgjvapGCW4Lo5UB1JJRXEM4YmTiEbyUyahKqw= thomas@Arcturus
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java
new file mode 100644
index 0000000..7b03440
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/JSchSshTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.util.Arrays;
+
+import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.util.FS;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+public class JSchSshTest extends SshTestBase {
+
+	private class TestSshSessionFactory extends JschConfigSessionFactory {
+
+		@Override
+		protected void configure(Host hc, Session session) {
+			// Nothing
+		}
+
+		@Override
+		public synchronized RemoteSession getSession(URIish uri,
+				CredentialsProvider credentialsProvider, FS fs, int tms)
+				throws TransportException {
+			return super.getSession(uri, credentialsProvider, fs, tms);
+		}
+
+		@Override
+		protected JSch createDefaultJSch(FS fs) throws JSchException {
+			JSch defaultJSch = super.createDefaultJSch(fs);
+			if (knownHosts.exists()) {
+				defaultJSch.setKnownHosts(knownHosts.getAbsolutePath());
+			}
+			return defaultJSch;
+		}
+	}
+
+	@Override
+	protected SshSessionFactory createSessionFactory() {
+		return new TestSshSessionFactory();
+	}
+
+	@Override
+	protected void installConfig(String... config) {
+		SshSessionFactory factory = getSessionFactory();
+		assertTrue(factory instanceof JschConfigSessionFactory);
+		JschConfigSessionFactory j = (JschConfigSessionFactory) factory;
+		try {
+			j.setConfig(createConfig(config));
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
+	private OpenSshConfig createConfig(String... content) throws IOException {
+		File configFile = new File(sshDir, Constants.CONFIG);
+		if (content != null) {
+			Files.write(configFile.toPath(), Arrays.asList(content));
+		}
+		return new OpenSshConfig(getTemporaryDirectory(), configFile);
+	}
+
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
index 19fcbfd..0760585 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/OpenSshConfigTest.java
@@ -67,6 +67,7 @@
 import org.junit.Test;
 
 import com.jcraft.jsch.ConfigRepository;
+import com.jcraft.jsch.ConfigRepository.Config;
 
 public class OpenSshConfigTest extends RepositoryTestCase {
 	private File home;
@@ -164,6 +165,20 @@ public void testQuoteParsing() throws Exception {
 	}
 
 	@Test
+	public void testCaseInsensitiveKeyLookup() throws Exception {
+		config("Host orcz\n" + "Port 29418\n"
+				+ "\tHostName repo.or.cz\nStrictHostKeyChecking yes\n");
+		final Host h = osc.lookup("orcz");
+		Config c = h.getConfig();
+		String exactCase = c.getValue("StrictHostKeyChecking");
+		assertEquals("yes", exactCase);
+		assertEquals(exactCase, c.getValue("stricthostkeychecking"));
+		assertEquals(exactCase, c.getValue("STRICTHOSTKEYCHECKING"));
+		assertEquals(exactCase, c.getValue("sTrIcThostKEYcheckING"));
+		assertNull(c.getValue("sTrIcThostKEYcheckIN"));
+	}
+
+	@Test
 	public void testAlias_DoesNotMatch() throws Exception {
 		config("Host orcz\n" + "Port 29418\n" + "\tHostName repo.or.cz\n");
 		final Host h = osc.lookup("repo.or.cz");
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java
new file mode 100644
index 0000000..0d625f3
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/SshTestBase.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.TransportException;
+import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.junit.RepositoryTestCase;
+import org.eclipse.jgit.junit.ssh.SshTestGitServer;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Test;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.KeyPair;
+
+public abstract class SshTestBase extends RepositoryTestCase {
+
+	protected static final String TEST_USER = "testuser";
+
+	protected File sshDir;
+
+	protected File privateKey1;
+
+	protected File privateKey2;
+
+	private SshTestGitServer server;
+
+	private SshSessionFactory factory;
+
+	protected int testPort;
+
+	protected File knownHosts;
+
+	private File homeDir;
+
+	@Override
+	public void setUp() throws Exception {
+		super.setUp();
+		writeTrashFile("file.txt", "something");
+		try (Git git = new Git(db)) {
+			git.add().addFilepattern("file.txt").call();
+			git.commit().setMessage("Initial commit").call();
+		}
+		mockSystemReader.setProperty("user.home",
+				getTemporaryDirectory().getAbsolutePath());
+		mockSystemReader.setProperty("HOME",
+				getTemporaryDirectory().getAbsolutePath());
+		homeDir = FS.DETECTED.userHome();
+		FS.DETECTED.setUserHome(getTemporaryDirectory().getAbsoluteFile());
+		sshDir = new File(getTemporaryDirectory(), ".ssh");
+		assertTrue(sshDir.mkdir());
+		File serverDir = new File(getTemporaryDirectory(), "srv");
+		assertTrue(serverDir.mkdir());
+		// Create two key pairs. Let's not call them "id_rsa".
+		privateKey1 = new File(sshDir, "first_key");
+		privateKey2 = new File(sshDir, "second_key");
+		createKeyPair(privateKey1);
+		createKeyPair(privateKey2);
+		ByteArrayOutputStream publicHostKey = new ByteArrayOutputStream();
+		// Start a server with our test user and the first key.
+		server = new SshTestGitServer(TEST_USER, privateKey1.toPath(), db,
+				createHostKey(publicHostKey));
+		testPort = server.start();
+		assertTrue(testPort > 0);
+		knownHosts = new File(sshDir, "known_hosts");
+		Files.write(knownHosts.toPath(), Collections.singleton("[localhost]:"
+				+ testPort + ' '
+				+ publicHostKey.toString(StandardCharsets.US_ASCII.name())));
+		factory = createSessionFactory();
+		SshSessionFactory.setInstance(factory);
+	}
+
+	private static void createKeyPair(File privateKeyFile) throws Exception {
+		// Found no way to do this with MINA sshd except rolling it all
+		// ourselves...
+		JSch jsch = new JSch();
+		KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
+		try (OutputStream out = new FileOutputStream(privateKeyFile)) {
+			pair.writePrivateKey(out);
+		}
+		File publicKeyFile = new File(privateKeyFile.getParentFile(),
+				privateKeyFile.getName() + ".pub");
+		try (OutputStream out = new FileOutputStream(publicKeyFile)) {
+			pair.writePublicKey(out, TEST_USER);
+		}
+	}
+
+	private static byte[] createHostKey(OutputStream publicKey)
+			throws Exception {
+		JSch jsch = new JSch();
+		KeyPair pair = KeyPair.genKeyPair(jsch, KeyPair.RSA, 2048);
+		pair.writePublicKey(publicKey, "");
+		try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+			pair.writePrivateKey(out);
+			out.flush();
+			return out.toByteArray();
+		}
+	}
+
+	@After
+	public void shutdownServer() throws Exception {
+		if (server != null) {
+			server.stop();
+			server = null;
+		}
+		FS.DETECTED.setUserHome(homeDir);
+		SshSessionFactory.setInstance(null);
+		factory = null;
+	}
+
+	protected abstract SshSessionFactory createSessionFactory();
+
+	protected SshSessionFactory getSessionFactory() {
+		return factory;
+	}
+
+	protected abstract void installConfig(String... config);
+
+	@Test(expected = TransportException.class)
+	public void testSshCloneWithoutConfig() throws Exception {
+		cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
+				+ "/doesntmatter", null);
+	}
+
+	@Test
+	public void testSshCloneWithGlobalIdentity() throws Exception {
+		cloneWith(
+				"ssh://" + TEST_USER + "@localhost:" + testPort
+						+ "/doesntmatter",
+				null,
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithDefaultIdentity() throws Exception {
+		File idRsa = new File(privateKey1.getParentFile(), "id_rsa");
+		Files.copy(privateKey1.toPath(), idRsa.toPath());
+		// We expect the session factory to pick up these keys...
+		cloneWith("ssh://" + TEST_USER + "@localhost:" + testPort
+				+ "/doesntmatter", null);
+	}
+
+	@Test
+	public void testSshCloneWithConfig() throws Exception {
+		cloneWith("ssh://localhost/doesntmatter", null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithConfigEncryptedUnusedKey() throws Exception {
+		// Copy the encrypted test key from the bundle.
+		File encryptedKey = new File(sshDir, "id_dsa");
+		try (InputStream in = SshTestBase.class
+				.getResourceAsStream("id_dsa_test")) {
+			Files.copy(in, encryptedKey.toPath());
+		}
+		TestCredentialsProvider provider = new TestCredentialsProvider(
+				"testpass");
+		cloneWith("ssh://localhost/doesntmatter", provider, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+		assertEquals("CredentialsProvider should not have been called", 0,
+				provider.getLog().size());
+	}
+
+	@Test(expected = TransportException.class)
+	public void testSshCloneWithoutKnownHosts() throws Exception {
+		assertTrue("Could not delete known_hosts", knownHosts.delete());
+		cloneWith("ssh://localhost/doesntmatter", null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithoutKnownHostsWithProvider() throws Exception {
+		File copiedHosts = new File(knownHosts.getParentFile(),
+				"copiedKnownHosts");
+		assertTrue("Failed to rename known_hosts",
+				knownHosts.renameTo(copiedHosts));
+		TestCredentialsProvider provider = new TestCredentialsProvider();
+		cloneWith("ssh://localhost/doesntmatter", provider, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+		Map<URIish, List<CredentialItem>> messages = provider.getLog();
+		assertFalse("Expected user iteraction", messages.isEmpty());
+	}
+
+	@Test
+	public void testSftpCloneWithConfig() throws Exception {
+		cloneWith("sftp://localhost/.git", null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test(expected = TransportException.class)
+	public void testSshCloneWithConfigWrongKey() throws Exception {
+		cloneWith("ssh://localhost/doesntmatter", null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey2.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithWrongUserNameInConfig() throws Exception {
+		// Bug 526778
+		cloneWith(
+				"ssh://" + TEST_USER + "@localhost:" + testPort
+						+ "/doesntmatter",
+				null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"User sombody_else", //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithWrongPortInConfig() throws Exception {
+		// Bug 526778
+		cloneWith(
+				"ssh://" + TEST_USER + "@localhost:" + testPort
+						+ "/doesntmatter",
+				null, //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port 22", //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithAliasInConfig() throws Exception {
+		// Bug 531118
+		cloneWith("ssh://git/doesntmatter", null, //
+				"Host git", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath(), "", //
+				"Host localhost", //
+				"HostName localhost", //
+				"Port 22", //
+				"User someone_else", //
+				"IdentityFile " + privateKey2.getAbsolutePath());
+	}
+
+	@Test
+	public void testSshCloneWithUnknownCiphersInConfig() throws Exception {
+		// Bug 535672
+		cloneWith("ssh://git/doesntmatter", null, //
+				"Host git", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath(), //
+				"Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr");
+	}
+
+	@Test
+	public void testSshCloneWithUnknownHostKeyAlgorithmsInConfig()
+			throws Exception {
+		// Bug 535672
+		cloneWith("ssh://git/doesntmatter", null, //
+				"Host git", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath(), //
+				"HostKeyAlgorithms foobar,ssh-rsa,ssh-dss");
+	}
+
+	@Test
+	public void testSshCloneWithUnknownKexAlgorithmsInConfig()
+			throws Exception {
+		// Bug 535672
+		cloneWith("ssh://git/doesntmatter", null, //
+				"Host git", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath(), //
+				"KexAlgorithms foobar,diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521");
+	}
+
+	@Test
+	public void testSshCloneWithMinimalHostKeyAlgorithmsInConfig()
+			throws Exception {
+		// Bug 537790
+		cloneWith("ssh://git/doesntmatter", null, //
+				"Host git", //
+				"HostName localhost", //
+				"Port " + testPort, //
+				"User " + TEST_USER, //
+				"IdentityFile " + privateKey1.getAbsolutePath(), //
+				"HostKeyAlgorithms ssh-rsa,ssh-dss");
+	}
+
+	private void cloneWith(String uri, CredentialsProvider provider,
+			String... config) throws Exception {
+		installConfig(config);
+		File cloned = new File(getTemporaryDirectory(), "cloned");
+		CloneCommand clone = Git.cloneRepository().setCloneAllBranches(true)
+				.setDirectory(cloned).setURI(uri);
+		if (provider != null) {
+			clone.setCredentialsProvider(provider);
+		}
+		try (Git git = clone.call()) {
+			assertNotNull(git.getRepository().resolve("master"));
+			assertNotEquals(db.getWorkTree(),
+					git.getRepository().getWorkTree());
+			checkFile(new File(git.getRepository().getWorkTree(), "file.txt"),
+					"something");
+		}
+	}
+
+	private class TestCredentialsProvider extends CredentialsProvider {
+
+		private final List<String> stringStore;
+
+		private final Iterator<String> strings;
+
+		public TestCredentialsProvider(String... strings) {
+			if (strings == null || strings.length == 0) {
+				stringStore = Collections.emptyList();
+			} else {
+				stringStore = Arrays.asList(strings);
+			}
+			this.strings = stringStore.iterator();
+		}
+
+		@Override
+		public boolean isInteractive() {
+			return true;
+		}
+
+		@Override
+		public boolean supports(CredentialItem... items) {
+			return true;
+		}
+
+		@Override
+		public boolean get(URIish uri, CredentialItem... items)
+				throws UnsupportedCredentialItem {
+			System.out.println("URI: " + uri);
+			for (CredentialItem item : items) {
+				System.out.println(item.getClass().getSimpleName() + ' '
+						+ item.getPromptText());
+			}
+			logItems(uri, items);
+			for (CredentialItem item : items) {
+				if (item instanceof CredentialItem.InformationalMessage) {
+					continue;
+				}
+				if (item instanceof CredentialItem.YesNoType) {
+					((CredentialItem.YesNoType) item).setValue(true);
+				} else if (item instanceof CredentialItem.CharArrayType) {
+					if (strings.hasNext()) {
+						((CredentialItem.CharArrayType) item)
+								.setValue(strings.next().toCharArray());
+					} else {
+						return false;
+					}
+				} else if (item instanceof CredentialItem.StringType) {
+					if (strings.hasNext()) {
+						((CredentialItem.StringType) item)
+								.setValue(strings.next());
+					} else {
+						return false;
+					}
+				} else {
+					return false;
+				}
+			}
+			return true;
+		}
+
+		private Map<URIish, List<CredentialItem>> log = new LinkedHashMap<>();
+
+		private void logItems(URIish uri, CredentialItem... items) {
+			log.put(uri, Arrays.asList(items));
+		}
+
+		public Map<URIish, List<CredentialItem>> getLog() {
+			return log;
+		}
+	}
+}
diff --git a/pom.xml b/pom.xml
index 4bd9b38..d7daae9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -198,6 +198,7 @@
     <bundle-manifest>${project.build.directory}/META-INF/MANIFEST.MF</bundle-manifest>
 
     <jgit-last-release-version>4.11.0.201803080745-r</jgit-last-release-version>
+    <apache-sshd-version>2.0.0</apache-sshd-version>
     <jsch-version>0.1.54</jsch-version>
     <jzlib-version>1.1.1</jzlib-version>
     <javaewah-version>1.1.6</javaewah-version>