diff options
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org')
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(); } |