You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/03 19:55:07 UTC
(maven-wagon) 01/04: Move wagon-ssh onto the maintained JSch fork
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git
commit e81e5db48e0c863a6facf1b116629ad144fe532e
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Sat Aug 8 13:01:55 2026 +0200
Move wagon-ssh onto the maintained JSch fork
com.jcraft:jsch was last released in 2018 and never gained what current
servers and current keys need. It has no rsa-sha2-256 or rsa-sha2-512,
so RSA public key authentication fails against OpenSSH 8.8 and later,
which stopped accepting SHA-1 signatures by default in 2021. It also
cannot read the OpenSSH-v1 private key format, which ssh-keygen has
produced by default since OpenSSH 7.8. A key generated today is rejected
outright, encrypted or not.
com.github.mwiede:jsch is a maintained fork of the same code under the
same com.jcraft.jsch package, so the provider keeps its API.
The fork carries the agent itself, so the two jsch.agentproxy artifacts
go. Their ConnectorFactory picked between agent kinds for us; the fork
has no equivalent, so the choice is now explicit, and all three kinds
the old stack could reach are still tried: the OpenSSH agent named by
SSH_AUTH_SOCK, the Win32 OpenSSH agent, and Pageant. One difference is
worth knowing: reaching the SSH_AUTH_SOCK agent needs a Unix domain
socket, which the JDK provides only from Java 16, and the fork declares
no dependencies, so on Java 8 to 15 that agent is out of reach unless a
helper library is on the class path. The other two do not need one.
An agent holding no identities is now also skipped rather than being
installed as an empty repository.
Passing a passphrase needs care with the fork. getPrivateKey()
substitutes an empty passphrase when the settings name none, which the
old JSch quietly ignored, leaving an encrypted key to be unlocked later.
The fork rejects it instead, which would have failed every encrypted
key. Empty is therefore translated back to none before the key is added.
---
wagon-providers/wagon-ssh/pom.xml | 15 +---
.../providers/ssh/jsch/AbstractJschWagon.java | 98 ++++++++++++++++++----
2 files changed, 83 insertions(+), 30 deletions(-)
diff --git a/wagon-providers/wagon-ssh/pom.xml b/wagon-providers/wagon-ssh/pom.xml
index 16bb4a45..e5cb463c 100644
--- a/wagon-providers/wagon-ssh/pom.xml
+++ b/wagon-providers/wagon-ssh/pom.xml
@@ -35,19 +35,10 @@ under the License.
<dependencies>
<dependency>
- <groupId>com.jcraft</groupId>
+ <!-- maintained fork of com.jcraft:jsch, same com.jcraft.jsch package, with the SSH agent built in -->
+ <groupId>com.github.mwiede</groupId>
<artifactId>jsch</artifactId>
- <version>0.1.55</version>
- </dependency>
- <dependency>
- <groupId>com.jcraft</groupId>
- <artifactId>jsch.agentproxy.connector-factory</artifactId>
- <version>0.0.9</version>
- </dependency>
- <dependency>
- <groupId>com.jcraft</groupId>
- <artifactId>jsch.agentproxy.jsch</artifactId>
- <version>0.0.9</version>
+ <version>2.28.6</version>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
diff --git a/wagon-providers/wagon-ssh/src/main/java/org/apache/maven/wagon/providers/ssh/jsch/AbstractJschWagon.java b/wagon-providers/wagon-ssh/src/main/java/org/apache/maven/wagon/providers/ssh/jsch/AbstractJschWagon.java
index a7320923..987a069b 100644
--- a/wagon-providers/wagon-ssh/src/main/java/org/apache/maven/wagon/providers/ssh/jsch/AbstractJschWagon.java
+++ b/wagon-providers/wagon-ssh/src/main/java/org/apache/maven/wagon/providers/ssh/jsch/AbstractJschWagon.java
@@ -32,22 +32,24 @@
import java.util.List;
import java.util.Properties;
+import com.jcraft.jsch.AgentConnector;
+import com.jcraft.jsch.AgentIdentityRepository;
+import com.jcraft.jsch.AgentProxyException;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.HostKeyRepository;
import com.jcraft.jsch.IdentityRepository;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.PageantConnector;
import com.jcraft.jsch.Proxy;
import com.jcraft.jsch.ProxyHTTP;
import com.jcraft.jsch.ProxySOCKS5;
+import com.jcraft.jsch.SSHAgentConnector;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UIKeyboardInteractive;
import com.jcraft.jsch.UserInfo;
-import com.jcraft.jsch.agentproxy.AgentProxyException;
-import com.jcraft.jsch.agentproxy.Connector;
-import com.jcraft.jsch.agentproxy.ConnectorFactory;
-import com.jcraft.jsch.agentproxy.RemoteIdentityRepository;
+import com.jcraft.jsch.WindowsSSHAgentConnector;
import org.apache.maven.wagon.CommandExecutionException;
import org.apache.maven.wagon.CommandExecutor;
import org.apache.maven.wagon.ResourceDoesNotExistException;
@@ -120,21 +122,11 @@ public void openConnectionInternal() throws AuthenticationException {
// can only pick one method of authentication
if (privateKey != null && privateKey.exists()) {
- fireSessionDebug("Using private key: " + privateKey);
- try {
- sch.addIdentity(privateKey.getAbsolutePath(), authenticationInfo.getPassphrase());
- } catch (JSchException e) {
- throw new AuthenticationException("Cannot connect. Reason: " + e.getMessage(), e);
- }
+ useIdentityFile(sch, privateKey);
} else {
- try {
- Connector connector = ConnectorFactory.getDefault().createConnector();
- if (connector != null) {
- IdentityRepository repo = new RemoteIdentityRepository(connector);
- sch.setIdentityRepository(repo);
- }
- } catch (AgentProxyException e) {
- fireSessionDebug("Unable to connect to agent: " + e.toString());
+ IdentityRepository agent = agentIdentityRepository();
+ if (agent != null) {
+ sch.setIdentityRepository(agent);
}
}
@@ -247,6 +239,76 @@ public void openConnectionInternal() throws AuthenticationException {
}
}
+ private void useIdentityFile(JSch sch, File privateKey) throws AuthenticationException {
+ fireSessionDebug("Using private key: " + privateKey);
+
+ // getPrivateKey() substitutes an empty passphrase when the settings name none, which is
+ // indistinguishable from "this key is not encrypted". Handing that on rejects every encrypted key
+ // outright. Passing null instead lets an unencrypted key through and leaves an encrypted one to be
+ // unlocked when it is used, which is where the user can still be asked for the passphrase.
+ String passphrase = authenticationInfo.getPassphrase();
+ if (passphrase != null && passphrase.isEmpty()) {
+ passphrase = null;
+ }
+
+ try {
+ sch.addIdentity(privateKey.getAbsolutePath(), passphrase);
+ } catch (JSchException e) {
+ throw new AuthenticationException("Cannot connect. Reason: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * The identities held by a running SSH agent, or <code>null</code> when no agent can be reached or the
+ * one reached holds none, in which case the caller falls back to a key file.
+ * <p>
+ * Three kinds of agent are tried in turn: the OpenSSH agent named by <code>SSH_AUTH_SOCK</code>, the
+ * Win32 OpenSSH agent, and Pageant. Every one of them is optional and each has its own prerequisites --
+ * the first needs a Unix domain socket, which the JDK itself provides only from Java 16, and Pageant
+ * needs JNA, which is not a dependency of this provider. Each is therefore both constructed and used
+ * inside its own guard, so that a missing class is one skipped agent rather than a failure to load.
+ */
+ private IdentityRepository agentIdentityRepository() {
+ IdentityRepository repository = identitiesFrom(SSHAgentConnector::new);
+ if (repository == null) {
+ repository = identitiesFrom(WindowsSSHAgentConnector::new);
+ }
+ if (repository == null) {
+ repository = identitiesFrom(PageantConnector::new);
+ }
+ return repository;
+ }
+
+ /**
+ * Creates one kind of {@link AgentConnector}, in a way that must only be invoked from inside
+ * {@link #identitiesFrom}: the classes involved may be absent at runtime, so the first mention of one
+ * has to sit where a {@link LinkageError} is caught.
+ */
+ private interface AgentConnectorFactory {
+ AgentConnector create() throws AgentProxyException;
+ }
+
+ private IdentityRepository identitiesFrom(AgentConnectorFactory factory) {
+ try {
+ AgentConnector connector = factory.create();
+ if (!connector.isAvailable()) {
+ return null;
+ }
+
+ AgentIdentityRepository repository = new AgentIdentityRepository(connector);
+ if (repository.getIdentities().isEmpty()) {
+ fireSessionDebug(connector.getName() + " holds no identities");
+ return null;
+ }
+
+ fireSessionDebug("Using the identities held by " + connector.getName());
+ return repository;
+ } catch (AgentProxyException | RuntimeException | LinkageError e) {
+ fireSessionDebug("Unable to use an SSH agent: " + e);
+ return null;
+ }
+ }
+
public void closeConnection() {
if (session != null) {
session.disconnect();