diff options
author | Thomas Wolf <thomas.wolf@paranor.ch> | 2021-10-20 09:51:43 +0200 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2021-11-03 23:45:33 +0100 |
commit | 68017a029cb6b8648b29ae695c9e614d1f7a9770 (patch) | |
tree | a870569b7b73821e113afc7acafbc80f3534b951 /org.eclipse.jgit.ssh.apache | |
parent | c04884fc9166fe491745fa51bd7540ff36ce6e7c (diff) | |
download | jgit-68017a029cb6b8648b29ae695c9e614d1f7a9770.tar.gz jgit-68017a029cb6b8648b29ae695c9e614d1f7a9770.zip |
sshd: prepare for using an SSH agent
Add interfaces Connector and ConnectorFactory. A "connector" is just
something that knows how to connect to an ssh-agent and then can make
simple synchronous RPC-style requests (request-reply).
Add a way to customize an SshdSessionFactory with a ConnectorFactory.
Provide a default setup using the Java ServiceLoader mechanism to
discover an ConnectorFactory.
Implement an SshAgentClient in the internal part. Unfortunately we
cannot re-use the implementation in Apache MINA sshd: it's hard-wired
to Apache Tomcat APR, and it's also buggy.
No behavior changes yet since there is nothing that would provide an
actual ConnectorFactory. So for Apache MINA sshd, the SshAgentFactory
remains null as before.
Change-Id: I963a3d181357df2bdb66298bc702f2b9a6607a30
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
Diffstat (limited to 'org.eclipse.jgit.ssh.apache')
12 files changed, 719 insertions, 4 deletions
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index 1f48841bc3..339ba3aae2 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -23,6 +23,7 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="6.0.0";x-inter org.apache.sshd.common.signature, org.apache.sshd.common.util.buffer, org.eclipse.jgit.transport", + org.eclipse.jgit.internal.transport.sshd.agent;version="6.0.0";x-internal:=true, org.eclipse.jgit.internal.transport.sshd.auth;version="6.0.0";x-internal:=true, org.eclipse.jgit.internal.transport.sshd.proxy;version="6.0.0";x-friends:="org.eclipse.jgit.ssh.apache.test", org.eclipse.jgit.transport.sshd;version="6.0.0"; @@ -31,7 +32,8 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="6.0.0";x-inter org.apache.sshd.common.keyprovider, org.eclipse.jgit.util, org.apache.sshd.client.session, - org.apache.sshd.client.keyverifier" + org.apache.sshd.client.keyverifier", + org.eclipse.jgit.transport.sshd.agent;version="6.0.0" Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)", org.apache.sshd.agent;version="[2.7.0,2.8.0)", org.apache.sshd.client;version="[2.7.0,2.8.0)", diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties index defcbdcfc1..2bba736aad 100644 --- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties +++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties @@ -19,6 +19,7 @@ identityFileNoKey=No keys found in identity {0} identityFileMultipleKeys=Multiple key pairs found in identity {0} identityFileNotFound=Skipping identity ''{0}'': file not found identityFileUnsupportedFormat=Unsupported format in identity {0} +invalidSignatureAlgorithm=Signature algorithm ''{0}'' is not valid for a key of type ''{1}'' kexServerKeyInvalid=Server key did not validate keyEncryptedMsg=Key ''{0}'' is encrypted. Enter the passphrase to decrypt it. keyEncryptedPrompt=Passphrase @@ -84,6 +85,10 @@ serverIdTooLong=Server identification is longer than 255 characters (including l serverIdWithNul=Server identification contains a NUL character: {0} sessionCloseFailed=Closing the session failed sessionWithoutUsername=SSH session created without user name; cannot authenticate +sshAgentReplyLengthError=Invalid SSH agent reply message length {0} after command {1} +sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0} +sshAgentShortReadBuffer=Short read from SSH agent +sshAgentWrongNumberOfKeys=Invalid number of SSH agent keys: {0} sshClosingDown=Apache MINA sshd session factory is closing down; cannot create new ssh sessions on this factory sshCommandTimeout={0} timed out after {1} seconds while opening the channel sshProcessStillRunning={0} is not yet completed, cannot get exit code diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index fdb8cde670..71e8e61585 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -32,8 +32,10 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.future.ConnectFuture; @@ -100,6 +102,8 @@ public class JGitSshClient extends SshClient { private ProxyDataFactory proxyDatabase; + private Supplier<SshAgentFactory> agentFactorySupplier = () -> null; + @Override protected SessionFactory createSessionFactory() { // Override the parent's default @@ -368,6 +372,22 @@ public class JGitSshClient extends SshClient { return credentialsProvider; } + @Override + public SshAgentFactory getAgentFactory() { + return agentFactorySupplier.get(); + } + + @Override + protected void checkConfig() { + // The super class requires channel factories for agent forwarding if a + // factory for an SSH agent is set. We haven't implemented this yet, and + // we don't do SSH agent forwarding for now. Unfortunately, there is no + // way to bypass this check in the super class except making + // getAgentFactory() return null until after the check. + super.checkConfig(); + agentFactorySupplier = super::getAgentFactory; + } + /** * A {@link SessionFactory} to create our own specialized * {@link JGitClientSession}s. diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index c0f5719629..00ee62d6dd 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -1,3 +1,12 @@ +/* + * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ package org.eclipse.jgit.internal.transport.sshd; import org.eclipse.jgit.nls.NLS; @@ -39,6 +48,7 @@ public final class SshdText extends TranslationBundle { /***/ public String identityFileMultipleKeys; /***/ public String identityFileNotFound; /***/ public String identityFileUnsupportedFormat; + /***/ public String invalidSignatureAlgorithm; /***/ public String kexServerKeyInvalid; /***/ public String keyEncryptedMsg; /***/ public String keyEncryptedPrompt; @@ -96,6 +106,10 @@ public final class SshdText extends TranslationBundle { /***/ public String serverIdWithNul; /***/ public String sessionCloseFailed; /***/ public String sessionWithoutUsername; + /***/ public String sshAgentReplyLengthError; + /***/ public String sshAgentReplyUnexpected; + /***/ public String sshAgentShortReadBuffer; + /***/ public String sshAgentWrongNumberOfKeys; /***/ public String sshClosingDown; /***/ public String sshCommandTimeout; /***/ public String sshProcessStillRunning; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java new file mode 100644 index 0000000000..9984f99763 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd.agent; + +import java.util.Iterator; +import java.util.ServiceLoader; + +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; + +/** + * Provides a {@link ConnectorFactory} obtained via the {@link ServiceLoader}. + */ +public final class ConnectorFactoryProvider { + + private static final ConnectorFactory FACTORY = loadDefaultFactory(); + + private static ConnectorFactory loadDefaultFactory() { + ServiceLoader<ConnectorFactory> loader = ServiceLoader + .load(ConnectorFactory.class); + Iterator<ConnectorFactory> iter = loader.iterator(); + while (iter.hasNext()) { + ConnectorFactory candidate = iter.next(); + if (candidate.isSupported()) { + return candidate; + } + } + return null; + + } + + private ConnectorFactoryProvider() { + // No instantiation + } + + /** + * Retrieves the default {@link ConnectorFactory} obtained via the + * {@link ServiceLoader}. + * + * @return the {@link ConnectorFactory}, or {@code null} if none. + */ + public static ConnectorFactory getDefaultFactory() { + return FACTORY; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java new file mode 100644 index 0000000000..1ed2ab9d78 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd.agent; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.SshAgentServer; +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.channel.ChannelFactory; +import org.apache.sshd.common.session.ConnectionService; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; + +/** + * A factory for creating {@link SshAgentClient}s. + */ +public class JGitSshAgentFactory implements SshAgentFactory { + + private final @NonNull ConnectorFactory factory; + + private final File homeDir; + + /** + * Creates a new {@link JGitSshAgentFactory}. + * + * @param factory + * {@link JGitSshAgentFactory} to wrap + * @param homeDir + * for obtaining the current local user's home directory + */ + public JGitSshAgentFactory(@NonNull ConnectorFactory factory, + File homeDir) { + this.factory = factory; + this.homeDir = homeDir; + } + + @Override + public List<ChannelFactory> getChannelForwardingFactories( + FactoryManager manager) { + // No agent forwarding supported yet. + return Collections.emptyList(); + } + + @Override + public SshAgent createClient(FactoryManager manager) throws IOException { + // sshd 2.8.0 will pass us the session here. At that point, we can get + // the HostConfigEntry and extract and handle the IdentityAgent setting. + // For now, pass null to let the ConnectorFactory do its default + // behavior (Pageant on Windows, SSH_AUTH_SOCK on Unixes with the + // jgit-builtin factory). + return new SshAgentClient(factory.create(null, homeDir)); + } + + @Override + public SshAgentServer createServer(ConnectionService service) + throws IOException { + // This should be called in a server only. + return null; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java new file mode 100644 index 0000000000..08483e4c20 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.internal.transport.sshd.agent; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.text.MessageFormat; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.BufferException; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.eclipse.jgit.internal.transport.sshd.SshdText; +import org.eclipse.jgit.transport.sshd.agent.Connector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A client for an SSH2 agent. This client supports only querying identities and + * signature requests. + * + * @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH + * Agent Protocol, RFC draft</a> + */ +public class SshAgentClient implements SshAgent { + + private static final Logger LOG = LoggerFactory + .getLogger(SshAgentClient.class); + + // OpenSSH limit + private static final int MAX_NUMBER_OF_KEYS = 2048; + + private final AtomicBoolean closed = new AtomicBoolean(); + + private final Connector connector; + + /** + * Creates a new {@link SshAgentClient} implementing the SSH2 ssh agent + * protocol, using the given {@link Connector} to connect to the SSH agent + * and to exchange messages. + * + * @param connector + * {@link Connector} to use + */ + public SshAgentClient(Connector connector) { + this.connector = connector; + } + + private boolean open(boolean debugging) throws IOException { + if (closed.get()) { + if (debugging) { + LOG.debug("SSH agent connection already closed"); //$NON-NLS-1$ + } + return false; + } + boolean connected = connector != null && connector.connect(); + if (!connected) { + if (debugging) { + LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$ + } + } + return connected; + } + + @Override + public void close() throws IOException { + if (!closed.getAndSet(true) && connector != null) { + connector.close(); + } + } + + @Override + public Iterable<? extends Map.Entry<PublicKey, String>> getIdentities() + throws IOException { + boolean debugging = LOG.isDebugEnabled(); + if (!open(debugging)) { + return Collections.emptyList(); + } + if (debugging) { + LOG.debug("Requesting identities from SSH agent"); //$NON-NLS-1$ + } + try { + Buffer reply = rpc( + SshAgentConstants.SSH2_AGENTC_REQUEST_IDENTITIES); + byte cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH2_AGENT_IDENTITIES_ANSWER) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + int numberOfKeys = reply.getInt(); + if (numberOfKeys < 0 || numberOfKeys > MAX_NUMBER_OF_KEYS) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentWrongNumberOfKeys, + Integer.toString(numberOfKeys))); + } + if (numberOfKeys == 0) { + if (debugging) { + LOG.debug("SSH agent has no keys"); //$NON-NLS-1$ + } + return Collections.emptyList(); + } + if (debugging) { + LOG.debug("Got {} key(s) from the SSH agent", //$NON-NLS-1$ + Integer.toString(numberOfKeys)); + } + boolean tracing = LOG.isTraceEnabled(); + List<Map.Entry<PublicKey, String>> keys = new ArrayList<>( + numberOfKeys); + for (int i = 0; i < numberOfKeys; i++) { + PublicKey key = reply.getPublicKey(); + String comment = reply.getString(); + if (tracing) { + LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ + KeyUtils.getKeyType(key), + KeyUtils.getFingerPrint(key), comment); + } + keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment)); + } + return keys; + } catch (BufferException e) { + throw new SshException(SshdText.get().sshAgentShortReadBuffer, e); + } + } + + @Override + public Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key, + String algorithm, byte[] data) throws IOException { + boolean debugging = LOG.isDebugEnabled(); + String keyType = KeyUtils.getKeyType(key); + String signatureAlgorithm; + if (algorithm != null) { + if (!KeyUtils.getCanonicalKeyType(algorithm).equals(keyType)) { + throw new IllegalArgumentException(MessageFormat.format( + SshdText.get().invalidSignatureAlgorithm, algorithm, + keyType)); + } + signatureAlgorithm = algorithm; + } else { + signatureAlgorithm = keyType; + } + if (!open(debugging)) { + return null; + } + int flags = 0; + switch (signatureAlgorithm) { + case KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS: + case KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS: + flags = 4; + break; + case KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS: + case KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS: + flags = 2; + break; + default: + break; + } + ByteArrayBuffer msg = new ByteArrayBuffer(); + msg.putInt(0); + msg.putByte(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST); + msg.putPublicKey(key); + msg.putBytes(data); + msg.putInt(flags); + if (debugging) { + LOG.debug( + "sign({}): signing request to SSH agent for {} key, {} signature; flags={}", //$NON-NLS-1$ + session, keyType, signatureAlgorithm, + Integer.toString(flags)); + } + Buffer reply = rpc(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST, + msg.getCompactData()); + byte cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH2_AGENT_SIGN_RESPONSE) { + throw new SshException( + MessageFormat.format(SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + try { + Buffer signatureReply = new ByteArrayBuffer(reply.getBytes()); + String actualAlgorithm = signatureReply.getString(); + byte[] signature = signatureReply.getBytes(); + if (LOG.isTraceEnabled()) { + LOG.trace( + "sign({}): signature reply from SSH agent for {} key: {} signature={}", //$NON-NLS-1$ + session, keyType, actualAlgorithm, + BufferUtils.toHex(':', signature)); + + } else if (LOG.isDebugEnabled()) { + LOG.debug( + "sign({}): signature reply from SSH agent for {} key, {} signature", //$NON-NLS-1$ + session, keyType, actualAlgorithm); + } + return new AbstractMap.SimpleImmutableEntry<>(actualAlgorithm, + signature); + } catch (BufferException e) { + throw new SshException(SshdText.get().sshAgentShortReadBuffer, e); + } + } + + private Buffer rpc(byte command, byte[] message) throws IOException { + return new ByteArrayBuffer(connector.rpc(command, message)); + } + + private Buffer rpc(byte command) throws IOException { + return new ByteArrayBuffer(connector.rpc(command)); + } + + @Override + public boolean isOpen() { + return !closed.get(); + } + + @Override + public void addIdentity(KeyPair key, String comment) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void removeIdentity(PublicKey key) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAllIdentities() throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index cad959c904..da99f56cb8 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -56,11 +56,14 @@ import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase; import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper; import org.eclipse.jgit.internal.transport.sshd.SshdText; +import org.eclipse.jgit.internal.transport.sshd.agent.ConnectorFactoryProvider; +import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.SshConfigStore; import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; import org.eclipse.jgit.util.FS; /** @@ -216,6 +219,11 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { new JGitUserInteraction(credentialsProvider)); client.setUserAuthFactories(getUserAuthFactories()); client.setKeyIdentityProvider(defaultKeysProvider); + ConnectorFactory connectors = getConnectorFactory(); + if (connectors != null) { + client.setAgentFactory( + new JGitSshAgentFactory(connectors, home)); + } // JGit-specific things: JGitSshClient jgitClient = (JGitSshClient) client; jgitClient.setKeyCache(getKeyCache()); @@ -437,6 +445,17 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { } /** + * Gets a {@link ConnectorFactory}. If this returns {@code null}, SSH agents + * are not supported. + * + * @return the factory, or {@code null} if no SSH agent support is desired + * @since 6.0 + */ + protected ConnectorFactory getConnectorFactory() { + return ConnectorFactoryProvider.getDefaultFactory(); + } + + /** * Gets the list of default user known hosts files. The default returns * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config * {@code UserKnownHostsFile} overrides this default. diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java index 2147c2bd58..7ed9b5ea3b 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2020, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at @@ -20,6 +20,7 @@ import java.util.function.Function; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.SshConfigStore; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; import org.eclipse.jgit.util.StringUtils; /** @@ -114,7 +115,7 @@ public final class SshdSessionFactoryBuilder { } /** - * A factory interface for creating a @link SshConfigStore}. + * A factory interface for creating a {@link SshConfigStore}. */ @FunctionalInterface public interface ConfigStoreFactory { @@ -233,6 +234,41 @@ public final class SshdSessionFactoryBuilder { } /** + * Sets an explicit {@link ConnectorFactory}. If {@code null}, there will be + * no support for SSH agents. + * <p> + * If not set, the created {@link SshdSessionFactory} will use the + * {@link java.util.ServiceLoader} to find an {@link ConnectorFactory}. + * </p> + * + * @param factory + * {@link ConnectorFactory} to use + * @return this {@link SshdSessionFactoryBuilder} + * @since 6.0 + */ + public SshdSessionFactoryBuilder setConnectorFactory( + ConnectorFactory factory) { + this.state.connectorFactory = factory; + this.state.connectorFactorySet = true; + return this; + } + + /** + * Removes a previously set {@link ConnectorFactory}. The created + * {@link SshdSessionFactory} will use the {@link java.util.ServiceLoader} + * to find an {@link ConnectorFactory}. This is also the default if + * {@link #setConnectorFactory(ConnectorFactory)} isn't called at all. + * + * @return this {@link SshdSessionFactoryBuilder} + * @since 6.0 + */ + public SshdSessionFactoryBuilder withDefaultConnectorFactory() { + this.state.connectorFactory = null; + this.state.connectorFactorySet = false; + return this; + } + + /** * Builds a {@link SshdSessionFactory} as configured, using the given * {@link KeyCache} for caching keys. * <p> @@ -277,6 +313,10 @@ public final class SshdSessionFactoryBuilder { BiFunction<File, File, ServerKeyDatabase> serverKeyDatabaseCreator; + ConnectorFactory connectorFactory; + + boolean connectorFactorySet; + State copy() { State c = new State(); c.proxyDataFactory = proxyDataFactory; @@ -290,6 +330,8 @@ public final class SshdSessionFactoryBuilder { c.defaultKeyFileFinder = defaultKeyFileFinder; c.defaultKeysProvider = defaultKeysProvider; c.serverKeyDatabaseCreator = serverKeyDatabaseCreator; + c.connectorFactory = connectorFactory; + c.connectorFactorySet = connectorFactorySet; return c; } @@ -388,6 +430,15 @@ public final class SshdSessionFactoryBuilder { return super.createSshConfigStore(homeDir, configFile, localUserName); } + + @Override + protected ConnectorFactory getConnectorFactory() { + if (connectorFactorySet) { + return connectorFactory; + } + // Use default via ServiceLoader + return super.getConnectorFactory(); + } } } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java new file mode 100644 index 0000000000..71ddc3b003 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport.sshd.agent; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Objects; + +import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.eclipse.jgit.internal.transport.sshd.SshdText; + +/** + * Provides some utility methods for implementing {@link Connector}s. + * + * @since 6.0 + */ +public abstract class AbstractConnector implements Connector { + + // A somewhat sane lower bound for the maximum reply length + private static final int MIN_REPLY_LENGTH = 8 * 1024; + + /** + * Default maximum reply length. 256kB is the OpenSSH limit. + */ + protected static final int DEFAULT_MAX_REPLY_LENGTH = 256 * 1024; + + private final int maxReplyLength; + + /** + * Creates a new instance using the {@link #DEFAULT_MAX_REPLY_LENGTH}. + */ + protected AbstractConnector() { + this(DEFAULT_MAX_REPLY_LENGTH); + } + + /** + * Creates a new instance. + * + * @param maxReplyLength + * maximum number of payload bytes we're ready to accept + */ + protected AbstractConnector(int maxReplyLength) { + if (maxReplyLength < MIN_REPLY_LENGTH) { + throw new IllegalArgumentException( + "Maximum payload length too small"); //$NON-NLS-1$ + } + this.maxReplyLength = maxReplyLength; + } + + /** + * Retrieves the maximum message length this {@link AbstractConnector} is + * configured for. + * + * @return the maximum message length + */ + protected int getMaximumMessageLength() { + return this.maxReplyLength; + } + + /** + * Prepares a message for sending by inserting the command and message + * length. + * + * @param command + * SSH agent command the request is for + * @param message + * about to be sent, including the 5 spare bytes at the front + * @throws IllegalArgumentException + * if {@code message} has less than 5 bytes + */ + protected void prepareMessage(byte command, byte[] message) + throws IllegalArgumentException { + Objects.requireNonNull(message); + if (message.length < 5) { + // No translation; internal error + throw new IllegalArgumentException("Message buffer for " //$NON-NLS-1$ + + SshAgentConstants.getCommandMessageName(command) + + " must have at least 5 bytes; have only " //$NON-NLS-1$ + + message.length); + } + BufferUtils.putUInt(message.length - 4, message); + message[4] = command; + } + + /** + * Checks the received length of a reply. + * + * @param command + * SSH agent command the reply is for + * @param length + * length as received: number of payload bytes + * @return the length as an {@code int} + * @throws IOException + * if the length is invalid + */ + protected int toLength(byte command, byte[] length) + throws IOException { + long l = BufferUtils.getUInt(length); + if (l <= 0 || l > maxReplyLength - 4) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyLengthError, + Long.toString(l), + SshAgentConstants.getCommandMessageName(command))); + } + return (int) l; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java new file mode 100644 index 0000000000..b6da0866a0 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport.sshd.agent; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Simple interface for connecting to something and making RPC-style + * request-reply calls. + * + * @since 6.0 + */ +public interface Connector extends Closeable { + + /** + * Connects to an SSH agent if there is one running. If called when already + * connected just returns {@code true}. + * + * @return {@code true} if an SSH agent is available and connected, + * {@false} if no SSH agent is available + * @throws IOException + * if connecting to the SSH agent failed + */ + boolean connect() throws IOException; + + /** + * Performs a remote call to the SSH agent and returns the result. + * + * @param command + * to send + * @param message + * to send; must have at least 5 bytes, and must have 5 unused + * bytes at the front. + * @return the result received + * @throws IOException + * if an error occurs + */ + byte[] rpc(byte command, byte[] message) throws IOException; + + /** + * Performs a remote call sending only a command without any parameters to + * the SSH agent and returns the result. + * + * @param command + * to send + * @return the result received + * @throws IOException + * if an error occurs + */ + default byte[] rpc(byte command) throws IOException { + return rpc(command, new byte[5]); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java new file mode 100644 index 0000000000..fa725ab858 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0 which is available at + * https://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package org.eclipse.jgit.transport.sshd.agent; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.annotations.NonNull; + +/** + * A factory for creating {@link Connector}s. + * + * @since 6.0 + */ +public interface ConnectorFactory { + + /** + * Creates a new {@link Connector}. + * + * @param identityAgent + * identifies the wanted agent connection; if {@code null}, the + * factory is free to provide a {@link Connector} to a default + * agent. The value will typically come from the IdentityAgent + * setting in ~/.ssh/config. + * @param homeDir + * the current local user's home directory as configured in the + * {@link org.eclipse.jgit.transport.sshd.SshdSessionFactory} + * @return a new {@link Connector} + * @throws IOException + * if no connector can be created + */ + @NonNull + Connector create(String identityAgent, File homeDir) + throws IOException; + + /** + * Tells whether this {@link ConnectorFactory} is applicable on the + * currently running platform. + * + * @return {@code true} if the factory can be used, {@code false} otherwise + */ + boolean isSupported(); + + /** + * Retrieves a name for this factory. + * + * @return the name + */ + String getName(); + +} |