summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache/src/org
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org')
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java198
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java4
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java190
3 files changed, 370 insertions, 22 deletions
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
index bfe11cb745..96da0cccdd 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
@@ -13,13 +13,17 @@ import static java.text.MessageFormat.format;
import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
import java.io.IOException;
+import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
+import java.security.KeyPair;
import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -30,6 +34,7 @@ import java.util.stream.Collectors;
import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.SshAgentFactory;
+import org.apache.sshd.agent.SshAgentKeyConstraint;
import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity;
import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
@@ -41,8 +46,14 @@ import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey;
import org.apache.sshd.common.signature.Signature;
import org.apache.sshd.common.signature.SignatureFactoriesManager;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.StringUtils;
/**
@@ -55,6 +66,14 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
private HostConfigEntry hostConfig;
+ private boolean addKeysToAgent;
+
+ private boolean askBeforeAdding;
+
+ private String skProvider;
+
+ private SshAgentKeyConstraint[] constraints;
+
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
}
@@ -95,9 +114,130 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
ClientSession session, SignatureFactoriesManager manager)
throws Exception {
agent = getAgent(session);
+ if (agent != null) {
+ parseAddKeys(hostConfig);
+ if (addKeysToAgent) {
+ skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER);
+ }
+ }
return new KeyIterator(session, manager);
}
+ @Override
+ protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(
+ ClientSession session, String service) throws Exception {
+ PublicKeyIdentity result = getNextKey(session, service);
+ // This fixes SSHD-1231. Can be removed once we're using Apache MINA
+ // sshd > 2.8.0.
+ //
+ // See https://issues.apache.org/jira/browse/SSHD-1231
+ currentAlgorithms.clear();
+ return result;
+ }
+
+ private PublicKeyIdentity getNextKey(ClientSession session, String service)
+ throws Exception {
+ PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session,
+ service);
+ if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) {
+ KeyPair key = id.getKeyIdentity();
+ if (key != null && key.getPublic() != null
+ && key.getPrivate() != null) {
+ // We've just successfully loaded a key that wasn't in the
+ // agent. Add it to the agent.
+ //
+ // Keys are added after loading, as in OpenSSH. The alternative
+ // might be to add a key only after (partially) successful
+ // authentication?
+ PublicKey pk = key.getPublic();
+ String fingerprint = KeyUtils.getFingerPrint(pk);
+ String keyType = KeyUtils.getKeyType(key);
+ try {
+ // Check that the key is not in the agent already.
+ if (agentHasKey(pk)) {
+ return id;
+ }
+ if (askBeforeAdding
+ && (session instanceof JGitClientSession)) {
+ CredentialsProvider provider = ((JGitClientSession) session)
+ .getCredentialsProvider();
+ CredentialItem.YesNoType question = new CredentialItem.YesNoType(
+ format(SshdText
+ .get().pubkeyAuthAddKeyToAgentQuestion,
+ keyType, fingerprint));
+ boolean result = provider != null
+ && provider.supports(question)
+ && provider.get(getUri(), question);
+ if (!result || !question.getValue()) {
+ // Don't add the key.
+ return id;
+ }
+ }
+ SshAgentKeyConstraint[] rules = constraints;
+ if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) {
+ rules = Arrays.copyOf(rules, rules.length + 1);
+ rules[rules.length - 1] =
+ new SshAgentKeyConstraint.FidoProviderExtension(skProvider);
+ }
+ // Unfortunately a comment associated with the key is lost
+ // by Apache MINA sshd, and there is also no way to get the
+ // original file name for keys loaded from a file. So add it
+ // without comment.
+ agent.addIdentity(key, null, rules);
+ } catch (IOException e) {
+ // Do not re-throw: we don't want authentication to fail if
+ // we cannot add the key to the agent.
+ log.error(
+ format(SshdText.get().pubkeyAuthAddKeyToAgentError,
+ keyType, fingerprint),
+ e);
+ // Note that as of Win32-OpenSSH 8.6 and Pageant 0.76,
+ // neither can handle key constraints. Pageant fails
+ // gracefully, not adding the key and returning
+ // SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection
+ // without even returning a failure message, which violates
+ // the SSH agent protocol and makes all subsequent requests
+ // to the agent fail.
+ }
+ }
+ }
+ return id;
+ }
+
+ private boolean agentHasKey(PublicKey pk) throws IOException {
+ Iterable<? extends Map.Entry<PublicKey, String>> ids = agent
+ .getIdentities();
+ if (ids == null) {
+ return false;
+ }
+ Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator();
+ while (iter.hasNext()) {
+ if (KeyUtils.compareKeys(iter.next().getKey(), pk)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private URIish getUri() {
+ String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$
+ String userName = hostConfig.getUsername();
+ if (!StringUtils.isEmptyOrNull(userName)) {
+ uri += userName + '@';
+ }
+ uri += hostConfig.getHost();
+ int port = hostConfig.getPort();
+ if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) {
+ uri += ":" + port; //$NON-NLS-1$
+ }
+ try {
+ return new URIish(uri);
+ } catch (URISyntaxException e) {
+ log.error(e.getLocalizedMessage(), e);
+ }
+ return new URIish();
+ }
+
private SshAgent getAgent(ClientSession session) throws Exception {
FactoryManager manager = Objects.requireNonNull(
session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$
@@ -108,8 +248,52 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
return factory.createClient(session, manager);
}
+ private void parseAddKeys(HostConfigEntry config) {
+ String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT);
+ if (StringUtils.isEmptyOrNull(value)) {
+ addKeysToAgent = false;
+ return;
+ }
+ String[] values = value.split(","); //$NON-NLS-1$
+ List<SshAgentKeyConstraint> rules = new ArrayList<>(2);
+ switch (values[0]) {
+ case "yes": //$NON-NLS-1$
+ addKeysToAgent = true;
+ break;
+ case "no": //$NON-NLS-1$
+ addKeysToAgent = false;
+ break;
+ case "ask": //$NON-NLS-1$
+ addKeysToAgent = true;
+ askBeforeAdding = true;
+ break;
+ case "confirm": //$NON-NLS-1$
+ addKeysToAgent = true;
+ rules.add(SshAgentKeyConstraint.CONFIRM);
+ if (values.length > 1) {
+ int seconds = OpenSshConfigFile.timeSpec(values[1]);
+ if (seconds > 0) {
+ rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
+ }
+ }
+ break;
+ default:
+ int seconds = OpenSshConfigFile.timeSpec(values[0]);
+ if (seconds > 0) {
+ addKeysToAgent = true;
+ rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
+ }
+ break;
+ }
+ constraints = rules.toArray(new SshAgentKeyConstraint[0]);
+ }
+
@Override
protected void releaseKeys() throws IOException {
+ addKeysToAgent = false;
+ askBeforeAdding = false;
+ skProvider = null;
+ constraints = null;
try {
if (agent != null) {
try {
@@ -212,18 +396,4 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
};
}
}
-
- @Override
- protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(
- ClientSession session, String service) throws Exception {
- PublicKeyIdentity result = super.resolveAttemptedPublicKeyIdentity(
- session, service);
- // This fixes SSHD-1231. Can be removed once we're using Apache MINA
- // sshd > 2.8.0.
- //
- // See https://issues.apache.org/jira/browse/SSHD-1231
- currentAlgorithms.clear();
- return result;
- }
-
}
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 f7b6f6acaa..b8c94eea59 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
@@ -99,6 +99,8 @@ public final class SshdText extends TranslationBundle {
/***/ public String proxySocksUnexpectedMessage;
/***/ public String proxySocksUnexpectedVersion;
/***/ public String proxySocksUsernameTooLong;
+ /***/ public String pubkeyAuthAddKeyToAgentError;
+ /***/ public String pubkeyAuthAddKeyToAgentQuestion;
/***/ public String pubkeyAuthWrongCommand;
/***/ public String pubkeyAuthWrongKey;
/***/ public String pubkeyAuthWrongSignatureAlgorithm;
@@ -107,6 +109,8 @@ public final class SshdText extends TranslationBundle {
/***/ public String serverIdWithNul;
/***/ public String sessionCloseFailed;
/***/ public String sessionWithoutUsername;
+ /***/ public String sshAgentEdDSAFormatError;
+ /***/ public String sshAgentPayloadLengthError;
/***/ public String sshAgentReplyLengthError;
/***/ public String sshAgentReplyUnexpected;
/***/ public String sshAgentShortReadBuffer;
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
index 13ca351eab..49b0d4ad77 100644
--- 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
@@ -11,10 +11,12 @@ package org.eclipse.jgit.internal.transport.sshd.agent;
import java.io.IOException;
import java.security.KeyPair;
+import java.security.PrivateKey;
import java.security.PublicKey;
import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -25,19 +27,23 @@ import org.apache.sshd.agent.SshAgentConstants;
import org.apache.sshd.agent.SshAgentKeyConstraint;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
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.apache.sshd.common.util.io.der.DERParser;
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.
+ * A client for an SSH2 agent. This client supports querying identities,
+ * signature requests, and adding keys to an agent (with or without
+ * constraints). Removing keys is not supported, and the older SSH1 protocol is
+ * not supported.
*
* @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH
* Agent Protocol, RFC draft</a>
@@ -224,6 +230,180 @@ public class SshAgentClient implements SshAgent {
}
}
+ @Override
+ public void addIdentity(KeyPair key, String comment,
+ SshAgentKeyConstraint... constraints) throws IOException {
+ boolean debugging = LOG.isDebugEnabled();
+ if (!open(debugging)) {
+ return;
+ }
+
+ // Neither Pageant 0.76 nor Win32-OpenSSH 8.6 support command
+ // SSH2_AGENTC_ADD_ID_CONSTRAINED. Adding a key with constraints will
+ // fail. The only work-around for users is not to use "confirm" or "time
+ // spec" with AddKeysToAgent, and not to use sk-* keys.
+ //
+ // With a true OpenSSH SSH agent, key constraints work.
+ byte cmd = (constraints != null && constraints.length > 0)
+ ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED
+ : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY;
+ byte[] message = null;
+ ByteArrayBuffer msg = new ByteArrayBuffer();
+ try {
+ msg.putInt(0);
+ msg.putByte(cmd);
+ String keyType = KeyUtils.getKeyType(key);
+ if (KeyPairProvider.SSH_ED25519.equals(keyType)) {
+ // Apache MINA sshd 2.8.0 lacks support for writing ed25519
+ // private keys to a buffer.
+ putEd25519Key(msg, key);
+ } else {
+ msg.putKeyPair(key);
+ }
+ msg.putString(comment == null ? "" : comment); //$NON-NLS-1$
+ if (constraints != null) {
+ for (SshAgentKeyConstraint constraint : constraints) {
+ constraint.put(msg);
+ }
+ }
+ if (debugging) {
+ LOG.debug(
+ "addIdentity: adding {} key {} to SSH agent; comment {}", //$NON-NLS-1$
+ keyType, KeyUtils.getFingerPrint(key.getPublic()),
+ comment);
+ }
+ message = msg.getCompactData();
+ } finally {
+ // The message contains the private key data, so clear intermediary
+ // data ASAP.
+ msg.clear();
+ }
+ Buffer reply;
+ try {
+ reply = rpc(cmd, message);
+ } finally {
+ Arrays.fill(message, (byte) 0);
+ }
+ int replyLength = reply.available();
+ if (replyLength != 1) {
+ throw new SshException(MessageFormat.format(
+ SshdText.get().sshAgentReplyUnexpected,
+ MessageFormat.format(
+ SshdText.get().sshAgentPayloadLengthError,
+ Integer.valueOf(1), Integer.valueOf(replyLength))));
+
+ }
+ cmd = reply.getByte();
+ if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) {
+ throw new SshException(
+ MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
+ SshAgentConstants.getCommandMessageName(cmd)));
+ }
+ }
+
+ /**
+ * Writes an ed25519 {@link KeyPair} to a {@link Buffer}. OpenSSH specifies
+ * that it expects the 32 public key bytes, followed by 64 bytes formed by
+ * concatenating the 32 private key bytes with the 32 public key bytes.
+ *
+ * @param msg
+ * {@link Buffer} to write to
+ * @param key
+ * {@link KeyPair} to write
+ * @throws IOException
+ * if the private key cannot be written
+ */
+ private static void putEd25519Key(Buffer msg, KeyPair key)
+ throws IOException {
+ Buffer tmp = new ByteArrayBuffer(36);
+ tmp.putRawPublicKeyBytes(key.getPublic());
+ byte[] publicBytes = tmp.getBytes();
+ msg.putString(KeyPairProvider.SSH_ED25519);
+ msg.putBytes(publicBytes);
+ // Next is the concatenation of the 32 byte private key value with the
+ // 32 bytes of the public key.
+ PrivateKey pk = key.getPrivate();
+ String format = pk.getFormat();
+ if (!"PKCS#8".equalsIgnoreCase(format)) { //$NON-NLS-1$
+ throw new IOException(MessageFormat
+ .format(SshdText.get().sshAgentEdDSAFormatError, format));
+ }
+ byte[] privateBytes = null;
+ byte[] encoded = pk.getEncoded();
+ try {
+ privateBytes = asn1Parse(encoded, 32);
+ byte[] combined = Arrays.copyOf(privateBytes, 64);
+ Arrays.fill(privateBytes, (byte) 0);
+ privateBytes = combined;
+ System.arraycopy(publicBytes, 0, privateBytes, 32, 32);
+ msg.putBytes(privateBytes);
+ } finally {
+ if (privateBytes != null) {
+ Arrays.fill(privateBytes, (byte) 0);
+ }
+ Arrays.fill(encoded, (byte) 0);
+ }
+ }
+
+ /**
+ * Extracts the private key bytes from an encoded ed25519 private key by
+ * parsing the bytes as ASN.1 according to RFC 5958 (PKCS #8 encoding):
+ *
+ * <pre>
+ * OneAsymmetricKey ::= SEQUENCE {
+ * version Version,
+ * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ * privateKey PrivateKey,
+ * ...
+ * }
+ *
+ * Version ::= INTEGER
+ * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
+ * PrivateKey ::= OCTET STRING
+ *
+ * AlgorithmIdentifier ::= SEQUENCE {
+ * algorithm OBJECT IDENTIFIER,
+ * parameters ANY DEFINED BY algorithm OPTIONAL
+ * }
+ * </pre>
+ * <p>
+ * and RFC 8410: "... when encoding a OneAsymmetricKey object, the private
+ * key is wrapped in a CurvePrivateKey object and wrapped by the OCTET
+ * STRING of the 'privateKey' field."
+ * </p>
+ *
+ * <pre>
+ * CurvePrivateKey ::= OCTET STRING
+ * </pre>
+ *
+ * @param encoded
+ * encoded private key to extract the private key bytes from
+ * @param n
+ * number of bytes expected
+ * @return the extracted private key bytes; of length {@code n}
+ * @throws IOException
+ * if the private key cannot be extracted
+ * @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a>
+ * @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>
+ */
+ private static byte[] asn1Parse(byte[] encoded, int n) throws IOException {
+ byte[] privateKey = null;
+ try (DERParser byteParser = new DERParser(encoded);
+ DERParser oneAsymmetricKey = byteParser.readObject()
+ .createParser()) {
+ oneAsymmetricKey.readObject(); // skip version
+ oneAsymmetricKey.readObject(); // skip algorithm identifier
+ privateKey = oneAsymmetricKey.readObject().getValue();
+ // The last n bytes of this must be the private key bytes
+ return Arrays.copyOfRange(privateKey,
+ privateKey.length - n, privateKey.length);
+ } finally {
+ if (privateKey != null) {
+ Arrays.fill(privateKey, (byte) 0);
+ }
+ }
+ }
+
private Buffer rpc(byte command, byte[] message) throws IOException {
return new ByteArrayBuffer(connector.rpc(command, message));
}
@@ -238,12 +418,6 @@ public class SshAgentClient implements SshAgent {
}
@Override
- public void addIdentity(KeyPair key, String comment,
- SshAgentKeyConstraint... constraints) throws IOException {
- throw new UnsupportedOperationException();
- }
-
- @Override
public void removeIdentity(PublicKey key) throws IOException {
throw new UnsupportedOperationException();
}