Merge "Inline image diff version switcher button styles"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8793458..ed4cf5a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -4837,6 +4837,16 @@
   replicate = replication start
 ----
 
+[[ssh]]
+=== Section ssh
+
+[[ssh.clientImplementation]]ssh.clientImplementation::
++
+JCraft JSch client is supported in addition to Apache MINA SSH client.
+To use JSch client set the value to `JSCH`.
++
+By default, `APACHE`.
+
 [[sshd]]
 === Section sshd
 
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index adc9be5..747f761 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -318,6 +318,18 @@
   bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
 ----
 
+To run SSH tests using JSch ssh client:
+
+----
+  bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=JSCH //...
+----
+
+To run SSH tests using Apache MINA ssh client:
+
+----
+  bazel test --test_env=SSH_CLIENT_IMPLEMENTATION=APACHE //...
+----
+
 To run only tests that do not use SSH:
 
 ----
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 737f2a4..63601d2 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -84,6 +84,7 @@
 * mime4j:dom
 * mina:core
 * mina:sshd
+* mina:sshd-sftp
 * openid:consumer
 * openid:nekohtml
 * openid:xerces
@@ -2348,6 +2349,7 @@
 * jgit
 * jgit-archive
 * jgit-servlet
+* jgit-ssh-apache
 
 [[jgit_license]]
 ----
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 722d9f3..b05050d 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -48,6 +48,7 @@
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.AccessSection;
 import com.google.gerrit.entities.Account;
@@ -144,7 +145,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Provider;
-import com.jcraft.jsch.KeyPair;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
@@ -156,6 +156,7 @@
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.KeyPair;
 import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -563,7 +564,7 @@
         && (adminSshSession == null || userSshSession == null)) {
       // Create Ssh sessions
       KeyPair adminKeyPair = sshKeys.getKeyPair(admin);
-      GitUtil.initSsh(adminKeyPair);
+      SshSessionFactory.initSsh(adminKeyPair);
       Context ctx = newRequestContext(user);
       atrScope.set(ctx);
       userSshSession = ctx.getSession();
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index 28f67b8..5ee1a08 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -40,6 +40,7 @@
     "//lib:guava-retrying",
     "//lib:jgit",
     "//lib:jgit-ssh-jsch",
+    "//lib:jgit-ssh-apache",
     "//lib:jsch",
     "//lib/commons:compress",
     "//lib/commons:lang",
@@ -52,6 +53,7 @@
     "//lib/mina:sshd",
     "//lib:guava",
     "//lib/bouncycastle:bcpg",
+    "//lib/bouncycastle:bcpkix",
     "//lib/bouncycastle:bcprov",
     "//prolog:gerrit-prolog-common",
 ]
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index ae72793..94d329d 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -20,17 +20,11 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.primitives.Ints;
-import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.entities.Project;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.KeyPair;
-import com.jcraft.jsch.Session;
 import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
-import java.util.Properties;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.api.FetchCommand;
@@ -47,41 +41,15 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.FetchResult;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig.Host;
 import org.eclipse.jgit.transport.PushResult;
 import org.eclipse.jgit.transport.RefSpec;
 import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.util.FS;
 
 public class GitUtil {
   private static final AtomicInteger testRepoCount = new AtomicInteger();
   private static final int TEST_REPO_WINDOW_DAYS = 2;
 
-  public static void initSsh(KeyPair keyPair) {
-    final Properties config = new Properties();
-    config.put("StrictHostKeyChecking", "no");
-    JSch.setConfig(config);
-
-    // register a JschConfigSessionFactory that adds the private key as identity
-    // to the JSch instance of JGit so that SSH communication via JGit can
-    // succeed
-    SshSessionFactory.setInstance(
-        new JschConfigSessionFactory() {
-          @Override
-          protected void configure(Host hc, Session session) {
-            try {
-              final JSch jsch = getJSch(hc, FS.DETECTED);
-              jsch.addIdentity(
-                  "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
-            } catch (JSchException e) {
-              throw new RuntimeException(e);
-            }
-          }
-        });
-  }
-
   /**
    * Create a new {@link TestRepository} with a distinct commit clock.
    *
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index 6698657..054e523 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -14,27 +14,18 @@
 
 package com.google.gerrit.acceptance;
 
-import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
-import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
-import com.jcraft.jsch.ChannelExec;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.KeyPair;
-import com.jcraft.jsch.Session;
-import java.io.InputStream;
 import java.net.InetSocketAddress;
-import java.util.Scanner;
 
-public class SshSession {
-  private final TestSshKeys sshKeys;
-  private final InetSocketAddress addr;
-  private final TestAccount account;
-  private Session session;
-  private String error;
+public abstract class SshSession {
+  protected final TestSshKeys sshKeys;
+  protected final InetSocketAddress addr;
+  protected final TestAccount account;
+  protected String error;
 
   public SshSession(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
     this.sshKeys = sshKeys;
@@ -42,44 +33,13 @@
     this.account = account;
   }
 
-  public void open() throws Exception {
-    getSession();
-  }
+  public abstract void open() throws Exception;
 
-  @SuppressWarnings("resource")
-  public String exec(String command) throws Exception {
-    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
-    try {
-      channel.setCommand(command);
-      InputStream in = channel.getInputStream();
-      InputStream err = channel.getErrStream();
-      channel.connect();
+  public abstract void close();
 
-      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
-      error = s.hasNext() ? s.next() : null;
+  public abstract String exec(String command) throws Exception;
 
-      s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
-      return s.hasNext() ? s.next() : "";
-    } finally {
-      channel.disconnect();
-    }
-  }
-
-  @SuppressWarnings("resource")
-  public int execAndReturnStatus(String command) throws Exception {
-    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
-    try {
-      channel.setCommand(command);
-      InputStream err = channel.getErrStream();
-      channel.connect();
-
-      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
-      error = s.hasNext() ? s.next() : null;
-      return channel.getExitStatus();
-    } finally {
-      channel.disconnect();
-    }
-  }
+  public abstract int execAndReturnStatus(String command) throws Exception;
 
   private boolean hasError() {
     return error != null;
@@ -102,46 +62,23 @@
     assertThat(getError()).contains(error);
   }
 
-  public void close() {
-    if (session != null) {
-      session.disconnect();
-      session = null;
-    }
-  }
-
-  private Session getSession() throws Exception {
-    if (session == null) {
-      KeyPair keyPair = sshKeys.getKeyPair(account);
-      JSch jsch = new JSch();
-      jsch.addIdentity(
-          "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
-      String username =
-          account
-              .username()
-              .orElseThrow(
-                  () ->
-                      new IllegalStateException(
-                          "account " + account.accountId() + " must have a username to use SSH"));
-      session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
-      session.setConfig("StrictHostKeyChecking", "no");
-      session.connect();
-    }
-    return session;
-  }
-
   public String getUrl() {
-    checkState(session != null, "session must be opened");
     StringBuilder b = new StringBuilder();
     b.append("ssh://");
-    b.append(session.getUserName());
+    b.append(account.username().get());
     b.append("@");
-    b.append(session.getHost());
+    b.append(addr.getAddress().getHostAddress());
     b.append(":");
-    b.append(session.getPort());
+    b.append(addr.getPort());
     return b.toString();
   }
 
-  public TestAccount getAccount() {
-    return account;
+  protected String getUsername() {
+    return account
+        .username()
+        .orElseThrow(
+            () ->
+                new IllegalStateException(
+                    "account " + account.accountId() + " must have a username to use SSH"));
   }
 }
diff --git a/java/com/google/gerrit/acceptance/SshSessionJsch.java b/java/com/google/gerrit/acceptance/SshSessionJsch.java
new file mode 100644
index 0000000..86cc438
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/SshSessionJsch.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.net.InetSocketAddress;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Properties;
+import java.util.Scanner;
+import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
+import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+public class SshSessionJsch extends SshSession {
+
+  private Session session;
+
+  public static void initClient(KeyPair keyPair) {
+    Properties config = new Properties();
+    config.put("StrictHostKeyChecking", "no");
+    JSch.setConfig(config);
+
+    // register a JschConfigSessionFactory that adds the private key as identity
+    // to the JSch instance of JGit so that SSH communication via JGit can
+    // succeed
+    SshSessionFactory.setInstance(
+        new JschConfigSessionFactory() {
+          @Override
+          protected void configure(Host hc, Session session) {
+            try {
+              JSch jsch = getJSch(hc, FS.DETECTED);
+              jsch.addIdentity(
+                  "KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
+            } catch (JSchException | GeneralSecurityException | IOException e) {
+              throw new RuntimeException(e);
+            }
+          }
+        });
+  }
+
+  public static KeyPairGenerator initKeyPairGenerator() throws NoSuchAlgorithmException {
+    KeyPairGenerator gen;
+    gen = KeyPairGenerator.getInstance("RSA");
+    gen.initialize(512, new SecureRandom());
+    return gen;
+  }
+
+  public SshSessionJsch(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
+    super(sshKeys, addr, account);
+  }
+
+  @Override
+  public void open() throws Exception {
+    getJschSession();
+  }
+
+  @Override
+  public void close() {
+    if (session != null) {
+      session.disconnect();
+      session = null;
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Override
+  public String exec(String command) throws Exception {
+    ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
+    try {
+      channel.setCommand(command);
+      InputStream in = channel.getInputStream();
+      InputStream err = channel.getErrStream();
+      channel.connect();
+
+      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
+      error = s.hasNext() ? s.next() : null;
+
+      s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
+      return s.hasNext() ? s.next() : "";
+    } finally {
+      channel.disconnect();
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Override
+  public int execAndReturnStatus(String command) throws Exception {
+    ChannelExec channel = (ChannelExec) getJschSession().openChannel("exec");
+    try {
+      channel.setCommand(command);
+      InputStream err = channel.getErrStream();
+      channel.connect();
+
+      Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
+      error = s.hasNext() ? s.next() : null;
+      return channel.getExitStatus();
+    } finally {
+      channel.disconnect();
+    }
+  }
+
+  private Session getJschSession() throws Exception {
+    if (session == null) {
+      KeyPair keyPair = sshKeys.getKeyPair(account);
+      JSch jsch = new JSch();
+      jsch.addIdentity("KeyPair", privateKey(keyPair), TestSshKeys.publicKeyBlob(keyPair), null);
+      String username = getUsername();
+      session = jsch.getSession(username, addr.getAddress().getHostAddress(), addr.getPort());
+      session.setConfig("StrictHostKeyChecking", "no");
+      session.connect();
+    }
+    return session;
+  }
+
+  private static byte[] privateKey(KeyPair keyPair) throws IOException {
+    // unencrypted form of PKCS#8 file
+    JcaPKCS8Generator gen1 = new JcaPKCS8Generator(keyPair.getPrivate(), null);
+    PemObject obj1 = gen1.generate();
+    StringWriter sw1 = new StringWriter();
+    try (JcaPEMWriter pw = new JcaPEMWriter(sw1)) {
+      pw.writeObject(obj1);
+    }
+    return sw1.toString().getBytes(US_ASCII.name());
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/SshSessionMina.java b/java/com/google/gerrit/acceptance/SshSessionMina.java
new file mode 100644
index 0000000..4514f44
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/SshSessionMina.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.CharSink;
+import com.google.common.io.Files;
+import com.google.common.io.MoreFiles;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPairGenerator;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Scanner;
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
+import org.eclipse.jgit.transport.sshd.JGitKeyCache;
+import org.eclipse.jgit.transport.sshd.SshdSession;
+import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+public class SshSessionMina extends SshSession {
+  private static final int TIMEOUT = 100000;
+
+  private SshdSession session;
+
+  public static void initClient() {
+    JGitKeyCache keyCache = new JGitKeyCache();
+    SshdSessionFactory factory = new SshdSessionFactory(keyCache, new DefaultProxyDataFactory());
+    SshSessionFactory.setInstance(factory);
+  }
+
+  public static KeyPairGenerator initKeyPairGenerator()
+      throws GeneralSecurityException, InvalidKeySpecException, InvalidAlgorithmParameterException {
+    int size = 256;
+    KeyPairGenerator gen = SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM);
+    ECCurves curve = ECCurves.fromCurveSize(size);
+    if (curve == null) {
+      throw new InvalidKeySpecException("Unknown curve for key size=" + size);
+    }
+    gen.initialize(curve.getParameters());
+    return gen;
+  }
+
+  public SshSessionMina(TestSshKeys sshKeys, InetSocketAddress addr, TestAccount account) {
+    super(sshKeys, addr, account);
+  }
+
+  @Override
+  public void open() throws Exception {
+    getMinaSession();
+  }
+
+  @Override
+  public void close() {
+    if (session != null) {
+      session.disconnect();
+      session = null;
+    }
+  }
+
+  @SuppressWarnings("resource")
+  @Override
+  public String exec(String command) throws Exception {
+    Process process = getMinaSession().exec(command, TIMEOUT);
+    InputStream in = process.getInputStream();
+    InputStream err = process.getErrorStream();
+
+    Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
+    error = s.hasNext() ? s.next() : null;
+
+    s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
+    return s.hasNext() ? s.next() : "";
+  }
+
+  @SuppressWarnings("resource")
+  @Override
+  public int execAndReturnStatus(String command) throws Exception {
+    Process process = getMinaSession().exec(command, 0);
+    InputStream in = process.getInputStream();
+    InputStream err = process.getErrorStream();
+
+    Scanner s = new Scanner(err, UTF_8.name()).useDelimiter("\\A");
+    error = s.hasNext() ? s.next() : null;
+
+    s = new Scanner(in, UTF_8.name()).useDelimiter("\\A");
+    try {
+      return process.exitValue();
+    } catch (IllegalThreadStateException e) {
+      // SSH command was interrupted
+      return -1;
+    }
+  }
+
+  private SshdSession getMinaSession() throws Exception {
+    if (session == null) {
+      String username = getUsername();
+
+      URIish uri =
+          new URIish(
+              "ssh://"
+                  + username
+                  + "@"
+                  + addr.getAddress().getHostAddress()
+                  + ":"
+                  + addr.getPort());
+
+      // TODO(davido): Switch to memory only key resolving mode.
+      File userhome = Files.createTempDir();
+
+      FS fs = FS.DETECTED.setUserHome(userhome);
+      File sshDir = new File(userhome, ".ssh");
+      sshDir.mkdir();
+      OpenSSHKeyPairResourceWriter keyPairWriter = new OpenSSHKeyPairResourceWriter();
+      try (OutputStream out = new FileOutputStream(new File(sshDir, "id_ecdsa"))) {
+        keyPairWriter.writePrivateKey(sshKeys.getKeyPair(account), null, null, out);
+      }
+
+      // TODO(davido): Disable programmatically host key checking: "StrictHostKeyChecking: no" mode.
+      CharSink configFile = Files.asCharSink(new File(sshDir, "config"), UTF_8);
+      configFile.writeLines(Arrays.asList("Host *", "StrictHostKeyChecking no"));
+
+      JGitKeyCache keyCache = new JGitKeyCache();
+      try (SshdSessionFactory factory =
+          new SshdSessionFactory(keyCache, new DefaultProxyDataFactory())) {
+        factory.setHomeDirectory(userhome);
+        factory.setSshDirectory(sshDir);
+
+        session = factory.getSession(uri, null, fs, TIMEOUT);
+
+        session.addCloseListener(
+            future -> {
+              try {
+                MoreFiles.deleteRecursively(userhome.toPath(), ALLOW_INSECURE);
+              } catch (IOException e) {
+                e.printStackTrace();
+              }
+            });
+      }
+    }
+    return session;
+  }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
index 6c95360..277d219 100644
--- a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -18,19 +18,20 @@
 import static java.nio.charset.StandardCharsets.US_ASCII;
 
 import com.google.gerrit.acceptance.SshEnabled;
+import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.KeyPair;
 import java.io.ByteArrayOutputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
 import java.util.HashMap;
 import java.util.Map;
+import org.apache.sshd.common.config.keys.writer.openssh.OpenSSHKeyPairResourceWriter;
 
 @Singleton
 public class TestSshKeys {
@@ -86,27 +87,26 @@
 
   private KeyPair createKeyPair(Account.Id accountId, String username, @Nullable String email)
       throws Exception {
-    KeyPair keyPair = genSshKey();
+    KeyPair keyPair = SshSessionFactory.genSshKey();
     authorizedKeys.addKey(accountId, publicKey(keyPair, email));
     sshKeyCache.evict(username);
     return keyPair;
   }
 
-  public static KeyPair genSshKey() throws JSchException {
-    JSch jsch = new JSch();
-    return KeyPair.genKeyPair(jsch, KeyPair.ECDSA, 256);
-  }
-
   public static String publicKey(KeyPair sshKey, @Nullable String comment)
-      throws UnsupportedEncodingException {
-    ByteArrayOutputStream out = new ByteArrayOutputStream();
-    sshKey.writePublicKey(out, comment);
-    return out.toString(US_ASCII.name()).trim();
+      throws IOException, GeneralSecurityException {
+    return preparePublicKey(sshKey, comment).toString(US_ASCII.name()).trim();
   }
 
-  public static byte[] privateKey(KeyPair keyPair) {
+  public static byte[] publicKeyBlob(KeyPair sshKey) throws IOException, GeneralSecurityException {
+    return preparePublicKey(sshKey, null).toByteArray();
+  }
+
+  private static ByteArrayOutputStream preparePublicKey(KeyPair sshKey, String comment)
+      throws IOException, GeneralSecurityException {
+    OpenSSHKeyPairResourceWriter keyPairWriter = new OpenSSHKeyPairResourceWriter();
     ByteArrayOutputStream out = new ByteArrayOutputStream();
-    keyPair.writePrivateKey(out);
-    return out.toByteArray();
+    keyPairWriter.writePublicKey(sshKey, comment, out);
+    return out;
   }
 }
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index db730a6..895c7a0 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -19,7 +19,6 @@
 
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
-import com.google.gerrit.acceptance.SshSession;
 import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
 import com.google.gerrit.acceptance.testsuite.account.TestAccount;
 import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
@@ -82,7 +81,7 @@
   public AcceptanceTestRequestScope.Context setApiUser(TestAccount testAccount) {
     return atrScope.set(
         atrScope.newContext(
-            new SshSession(testSshKeys, sshAddress, testAccount),
+            SshSessionFactory.createSession(testSshKeys, sshAddress, testAccount),
             createIdentifiedUser(testAccount.accountId())));
   }
 
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
new file mode 100644
index 0000000..d5dd28a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/request/SshSessionFactory.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.request;
+
+import static com.google.gerrit.server.config.SshClientImplementation.getFromEnvironment;
+
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.SshSessionJsch;
+import com.google.gerrit.acceptance.SshSessionMina;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
+import java.net.InetSocketAddress;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+public class SshSessionFactory {
+  public static SshSession createSession(
+      TestSshKeys testSshKeys, InetSocketAddress sshAddress, TestAccount testAccount) {
+    return getFromEnvironment().isMina()
+        ? new SshSessionMina(testSshKeys, sshAddress, testAccount)
+        : new SshSessionJsch(testSshKeys, sshAddress, testAccount);
+  }
+
+  public static void initSsh(KeyPair keyPair) {
+    if (getFromEnvironment().isMina()) {
+      SshSessionMina.initClient();
+    } else {
+      SshSessionJsch.initClient(keyPair);
+    }
+  }
+
+  private SshSessionFactory() {}
+
+  public static KeyPair genSshKey() throws GeneralSecurityException {
+    return (getFromEnvironment().isMina()
+            ? SshSessionMina.initKeyPairGenerator()
+            : SshSessionJsch.initKeyPairGenerator())
+        .generateKeyPair();
+  }
+}
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index ee99702..cd3ebb9 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -32,7 +32,6 @@
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-servlet",
-        "//lib:jsch",
         "//lib:servlet-api",
         "//lib:soy",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 37c63a2..adfbdcc 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -30,6 +30,7 @@
         "//java/com/google/gerrit/sshd",
         "//lib:guava",
         "//lib:jgit",
+        "//lib:jgit-ssh-apache",
         "//lib:servlet-api",
         "//lib/flogger:api",
         "//lib/guice",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 2df4739..d03340b 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -103,6 +103,7 @@
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
+import com.google.gerrit.sshd.SshSessionFactoryInitializer;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.gerrit.sshd.commands.SequenceCommandsModule;
@@ -339,6 +340,7 @@
         });
     modules.add(new DefaultUrlFormatter.Module());
 
+    SshSessionFactoryInitializer.init(config);
     modules.add(SshKeyCacheImpl.module());
     modules.add(
         new AbstractModule() {
diff --git a/java/com/google/gerrit/httpd/raw/SshInfoServlet.java b/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
index 1605360..ec67b8b 100644
--- a/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
+++ b/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -16,11 +16,11 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.gerrit.server.ssh.HostKey;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.util.http.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.jcraft.jsch.HostKey;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.List;
@@ -59,14 +59,14 @@
 
   @Override
   protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
-    final List<HostKey> hostKeys = sshd.getHostKeys();
-    final String out;
+    List<HostKey> hostKeys = sshd.getHostKeys();
+    String out;
     if (!hostKeys.isEmpty()) {
       String host = hostKeys.get(0).getHost();
       String port = "22";
 
       if (host.contains(":")) {
-        final int p = host.lastIndexOf(':');
+        int p = host.lastIndexOf(':');
         port = host.substring(p + 1);
         host = host.substring(0, p);
       }
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index faedcb7..16eebf2 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -44,6 +44,7 @@
         "//lib:args4j",
         "//lib:guava",
         "//lib:jgit",
+        "//lib:jgit-ssh-apache",
         "//lib:protobuf",
         "//lib:servlet-api-without-neverlink",
         "//lib/auto:auto-value",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 16c9d27..07bab24 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -114,6 +114,7 @@
 import com.google.gerrit.sshd.SshHostKeyModule;
 import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
+import com.google.gerrit.sshd.SshSessionFactoryInitializer;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.IndexCommandsModule;
 import com.google.gerrit.sshd.commands.SequenceCommandsModule;
@@ -482,6 +483,7 @@
           });
     }
     modules.add(new DefaultUrlFormatter.Module());
+    SshSessionFactoryInitializer.init(config);
     if (sshd) {
       modules.add(SshKeyCacheImpl.module());
     } else {
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 9fa7456..404906d 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -97,7 +97,6 @@
         "//lib:guava-retrying",
         "//lib:jgit",
         "//lib:jgit-archive",
-        "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
         "//lib:protobuf",
diff --git a/java/com/google/gerrit/server/audit/BUILD b/java/com/google/gerrit/server/audit/BUILD
index 0870786..3faa259 100644
--- a/java/com/google/gerrit/server/audit/BUILD
+++ b/java/com/google/gerrit/server/audit/BUILD
@@ -46,7 +46,6 @@
         "//lib:guava-retrying",
         "//lib:jgit",
         "//lib:jgit-archive",
-        "//lib:jsch",
         "//lib:juniversalchardet",
         "//lib:mime-util",
         "//lib:protobuf",
diff --git a/java/com/google/gerrit/server/config/SshClientImplementation.java b/java/com/google/gerrit/server/config/SshClientImplementation.java
new file mode 100644
index 0000000..5811e4d
--- /dev/null
+++ b/java/com/google/gerrit/server/config/SshClientImplementation.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Enums;
+import com.google.common.base.Strings;
+
+/* SSH implementation to use by JGit SSH client transport protocol. */
+public enum SshClientImplementation {
+  /** JCraft JSch implementation. */
+  JSCH,
+
+  /** Apache MINA implementation. */
+  APACHE;
+
+  private static final String ENV_VAR = "SSH_CLIENT_IMPLEMENTATION";
+  private static final String SYS_PROP = "gerrit.sshClientImplementation";
+
+  @VisibleForTesting
+  public static SshClientImplementation getFromEnvironment() {
+    String value = System.getenv(ENV_VAR);
+    if (Strings.isNullOrEmpty(value)) {
+      value = System.getProperty(SYS_PROP);
+    }
+    if (Strings.isNullOrEmpty(value)) {
+      return APACHE;
+    }
+    SshClientImplementation client =
+        Enums.getIfPresent(SshClientImplementation.class, value).orNull();
+    if (!Strings.isNullOrEmpty(System.getenv(ENV_VAR))) {
+      checkArgument(
+          client != null, "Invalid value for env variable %s: %s", ENV_VAR, System.getenv(ENV_VAR));
+    } else {
+      checkArgument(
+          client != null,
+          "Invalid value for system property %s: %s",
+          SYS_PROP,
+          System.getProperty(SYS_PROP));
+    }
+    return client;
+  }
+
+  public boolean isMina() {
+    return this == APACHE;
+  }
+}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 1fde48c..1cb0bea 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -56,11 +56,11 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.ssh.HostKey;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.jcraft.jsch.HostKey;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
diff --git a/java/com/google/gerrit/server/ssh/HostKey.java b/java/com/google/gerrit/server/ssh/HostKey.java
new file mode 100644
index 0000000..9397612
--- /dev/null
+++ b/java/com/google/gerrit/server/ssh/HostKey.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ssh;
+
+public class HostKey {
+  private final String host;
+  private final byte[] key;
+
+  public HostKey(String host, byte[] key) {
+    this.host = host;
+    this.key = key;
+  }
+
+  public String getHost() {
+    return host;
+  }
+
+  public byte[] getKey() {
+    return key;
+  }
+}
diff --git a/java/com/google/gerrit/server/ssh/NoSshInfo.java b/java/com/google/gerrit/server/ssh/NoSshInfo.java
index 91a949b..a716398 100644
--- a/java/com/google/gerrit/server/ssh/NoSshInfo.java
+++ b/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.jcraft.jsch.HostKey;
 import java.util.Collections;
 import java.util.List;
 
diff --git a/java/com/google/gerrit/server/ssh/SshInfo.java b/java/com/google/gerrit/server/ssh/SshInfo.java
index 430846d..ec5a579 100644
--- a/java/com/google/gerrit/server/ssh/SshInfo.java
+++ b/java/com/google/gerrit/server/ssh/SshInfo.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.ssh;
 
-import com.jcraft.jsch.HostKey;
 import java.util.List;
 
 public interface SshInfo {
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index a5b88b4..0668c1e 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -4,6 +4,7 @@
     name = "sshd",
     srcs = glob(["**/*.java"]),
     visibility = ["//visibility:public"],
+    runtime_deps = ["//lib:jsch"],
     deps = [
         "//java/com/google/gerrit/common:annotations",
         "//java/com/google/gerrit/common:server",
@@ -28,7 +29,7 @@
         "//lib:guava",
         "//lib:jgit",
         "//lib:jgit-archive",
-        "//lib:jsch",
+        "//lib:jgit-ssh-apache",
         "//lib:servlet-api",
         "//lib/auto:auto-value",
         "//lib/auto:auto-value-annotations",
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index cd5a511..9ae8660 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -37,6 +37,7 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.ssh.HostKey;
 import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.ssh.SshListenAddresses;
@@ -44,8 +45,6 @@
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import com.jcraft.jsch.HostKey;
-import com.jcraft.jsch.JSchException;
 import java.io.File;
 import java.io.IOException;
 import java.net.InetAddress;
@@ -435,12 +434,7 @@
       byte[] keyBin = buf.getCompactData();
 
       for (String addr : advertised) {
-        try {
-          r.add(new HostKey(addr, keyBin));
-        } catch (JSchException e) {
-          logger.atWarning().log(
-              "Cannot format SSHD host key [%s]: %s", pub.getAlgorithm(), e.getMessage());
-        }
+        r.add(new HostKey(addr, keyBin));
       }
     }
 
diff --git a/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
new file mode 100644
index 0000000..1cdf923
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SshSessionFactoryInitializer.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import static com.google.gerrit.server.config.SshClientImplementation.APACHE;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
+import org.eclipse.jgit.transport.sshd.JGitKeyCache;
+import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
+import org.eclipse.jgit.util.FS;
+
+public class SshSessionFactoryInitializer {
+  public static void init(Config config) {
+    if (APACHE == config.getEnum("ssh", null, "clientImplementation", APACHE)) {
+      SshdSessionFactory factory =
+          new SshdSessionFactory(new JGitKeyCache(), new DefaultProxyDataFactory());
+      factory.setHomeDirectory(FS.DETECTED.userHome());
+      SshSessionFactory.setInstance(factory);
+    }
+  }
+
+  private SshSessionFactoryInitializer() {}
+}
diff --git a/java/com/google/gerrit/testing/TestLoggingActivator.java b/java/com/google/gerrit/testing/TestLoggingActivator.java
index a766429..6b5d8fd 100644
--- a/java/com/google/gerrit/testing/TestLoggingActivator.java
+++ b/java/com/google/gerrit/testing/TestLoggingActivator.java
@@ -31,6 +31,7 @@
 
           // Silence non-critical messages from MINA SSHD.
           .put("org.apache.mina", Level.WARN)
+          .put("org.apache.sshd.client", Level.WARN)
           .put("org.apache.sshd.common", Level.WARN)
           .put("org.apache.sshd.server", Level.WARN)
           .put("org.apache.sshd.common.keyprovider.FileKeyPairProvider", Level.INFO)
@@ -61,6 +62,8 @@
           // Silence non-critical messages from JGit.
           .put("org.eclipse.jgit.transport.PacketLineIn", Level.WARN)
           .put("org.eclipse.jgit.transport.PacketLineOut", Level.WARN)
+          .put("org.eclipse.jgit.internal.transport.sshd", Level.WARN)
+          .put("org.eclipse.jgit.util.FileUtils", Level.WARN)
           .put("org.eclipse.jgit.internal.storage.file.FileSnapshot", Level.WARN)
           .put("org.eclipse.jgit.util.FS", Level.WARN)
           .put("org.eclipse.jgit.util.SystemReader", Level.WARN)
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 1b55652..7495e63 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -75,6 +75,7 @@
 import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
 import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
 import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.entities.AccessSection;
@@ -150,9 +151,9 @@
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.jcraft.jsch.KeyPair;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.security.KeyPair;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
@@ -2001,7 +2002,7 @@
 
       // Add a new key
       sender.clear();
-      String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+      String newKey = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), admin.email());
       gApi.accounts().self().addSshKey(newKey);
       info = gApi.accounts().self().listSshKeys();
       assertThat(info).hasSize(2);
@@ -2023,7 +2024,7 @@
 
       // Add another new key
       sender.clear();
-      String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+      String newKey2 = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), admin.email());
       gApi.accounts().self().addSshKey(newKey2);
       info = gApi.accounts().self().listSshKeys();
       assertThat(info).hasSize(3);
@@ -2074,7 +2075,7 @@
 
       // Add a new key
       sender.clear();
-      String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), user.email());
+      String newKey = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), user.email());
       gApi.accounts().id(user.username()).addSshKey(newKey);
       info = gApi.accounts().id(user.username()).listSshKeys();
       assertThat(info).hasSize(2);
@@ -2103,7 +2104,7 @@
   @Test
   @UseSsh
   public void userCannotAddSshKeyToOtherAccount() throws Exception {
-    String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
+    String newKey = TestSshKeys.publicKey(SshSessionFactory.genSshKey(), admin.email());
     requestScopeOperations.setApiUser(user.id());
     assertThrows(AuthException.class, () -> gApi.accounts().id(admin.username()).addSshKey(newKey));
   }
diff --git a/lib/BUILD b/lib/BUILD
index 0110047..f924e4ca 100644
--- a/lib/BUILD
+++ b/lib/BUILD
@@ -54,6 +54,16 @@
 )
 
 java_library(
+    name = "jgit-ssh-apache",
+    data = ["//lib:LICENSE-jgit"],
+    visibility = ["//visibility:public"],
+    exports = ["@jgit//org.eclipse.jgit.ssh.apache:ssh-apache"],
+    runtime_deps = [
+        "//lib/mina:sshd-sftp",
+    ],
+)
+
+java_library(
     name = "jgit-archive",
     data = ["//lib:LICENSE-jgit"],
     visibility = ["//visibility:public"],
diff --git a/lib/mina/BUILD b/lib/mina/BUILD
index 70e7c1d..3f23263 100644
--- a/lib/mina/BUILD
+++ b/lib/mina/BUILD
@@ -13,6 +13,13 @@
 )
 
 java_library(
+    name = "sshd-sftp",
+    data = ["//lib:LICENSE-Apache2.0"],
+    visibility = ["//visibility:public"],
+    exports = ["@sshd-sftp//jar"],
+)
+
+java_library(
     name = "eddsa",
     data = ["//lib:LICENSE-CC0-1.0"],
     visibility = ["//visibility:public"],
diff --git a/lib/nongoogle_test.sh b/lib/nongoogle_test.sh
index f596164..272cfa9 100755
--- a/lib/nongoogle_test.sh
+++ b/lib/nongoogle_test.sh
@@ -40,6 +40,7 @@
 soy
 sshd-mina
 sshd-osgi
+sshd-sftp
 testcontainers
 truth
 truth-java8-extension
diff --git a/resources/log4j.properties b/resources/log4j.properties
index 28c0ee4..39246b3 100644
--- a/resources/log4j.properties
+++ b/resources/log4j.properties
@@ -21,6 +21,7 @@
 # Silence non-critical messages from MINA SSHD.
 #
 log4j.logger.org.apache.mina=WARN
+log4j.logger.org.apache.sshd.client=WARN
 log4j.logger.org.apache.sshd.common=WARN
 log4j.logger.org.apache.sshd.server=WARN
 log4j.logger.org.apache.sshd.common.keyprovider.FileKeyPairProvider=INFO
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index acb5346..c7398a8 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -188,6 +188,8 @@
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.junit/src')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/src')
         classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.jsch/resources')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/src')
+        classpathentry('src', 'modules/jgit/org.eclipse.jgit.ssh.apache/resources')
 
     def classpathentry(kind, path, src=None, out=None, exported=None, excluding=None):
         e = doc.createElement('classpathentry')
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 11710f0..3d04592 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -44,6 +44,12 @@
     )
 
     maven_jar(
+        name = "sshd-sftp",
+        artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
+        sha1 = "6eddfe8fdf59a3d9a49151e4177f8c1bebeb30c9",
+    )
+
+    maven_jar(
         name = "eddsa",
         artifact = "net.i2p.crypto:eddsa:0.3.0",
         sha1 = "1901c8d4d8bffb7d79027686cfb91e704217c3e1",