]> source.dussan.org Git - jgit.git/commitdiff
sshd: support the AddKeysToAgent ssh config 76/189376/3
authorThomas Wolf <thomas.wolf@paranor.ch>
Tue, 28 Dec 2021 18:54:30 +0000 (19:54 +0100)
committerThomas Wolf <thomas.wolf@paranor.ch>
Sun, 30 Jan 2022 16:13:46 +0000 (17:13 +0100)
Add parsing of the config. Implement the SSH agent protocol for adding
a key. In the pubkey authentication, add keys to the agent as soon as
they've been loaded successfully, before even attempting to use them
for authentication. OpenSSH does the same.

Bug: 577052
Change-Id: Id1c08d9676a74652256b22281c2f8fa0b6508fa6
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java

index 754944f2e320c9f5f871f1fc40d0d94a0a7981bd..2ba94f2057c8b016dee9f1a9ffc47485fd6a379d 100644 (file)
@@ -55,6 +55,7 @@ Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
  org.apache.sshd.common.config.keys;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.config.keys.loader;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.config.keys.loader.openssh.kdf;version="[2.8.0,2.9.0)",
+ org.apache.sshd.common.config.keys.u2f;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.digest;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.forward;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.future;version="[2.8.0,2.9.0)",
@@ -73,6 +74,7 @@ Import-Package: net.i2p.crypto.eddsa;version="[0.3.0,0.4.0)",
  org.apache.sshd.common.util.buffer;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.util.closeable;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.util.io;version="[2.8.0,2.9.0)",
+ org.apache.sshd.common.util.io.der;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.util.io.functors;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.util.io.resource;version="[2.8.0,2.9.0)",
  org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)",
index 4b12db5d516d6613deebaaf1720ae4525b40dd51..b529e5b5aeb0aaf5f4b79189bd3d4663b1050147 100644 (file)
@@ -78,6 +78,8 @@ proxySocksPasswordTooLong=Password for proxy {0} must be at most 255 bytes long,
 proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2}
 proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0}
 proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2}
+pubkeyAuthAddKeyToAgentError=Could not add {0} key with fingerprint {1} to the SSH agent
+pubkeyAuthAddKeyToAgentQuestion=Add the {0} key with fingerprint {1} to the SSH agent?
 pubkeyAuthWrongCommand=Public key authentication received unknown SSH command {0} from {1} ({2})
 pubkeyAuthWrongKey=Public key authentication received wrong key; sent {0}, got back {1} from {2} ({3})
 pubkeyAuthWrongSignatureAlgorithm=Public key authentication requested signature type {0} but got back {1} from {2} ({3})
@@ -86,6 +88,8 @@ 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
+sshAgentEdDSAFormatError=Cannot add ed25519 key to the SSH agent because it is encoded as {0} instead of PKCS#8
+sshAgentPayloadLengthError=Expected {0,choice,0#no bytes|1#one byte|1<{0} bytes} but got {1}
 sshAgentReplyLengthError=Invalid SSH agent reply message length {0} after command {1}
 sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0}
 sshAgentShortReadBuffer=Short read from SSH agent
index bfe11cb74558c645c630d0ec8337538571e4f94e..96da0cccdd86f581efe2292a5e017f6dcea30aa3 100644 (file)
@@ -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;
-       }
-
 }
index f7b6f6acaa06a57afdda7e6ed0a3df32ad33760c..b8c94eea59d6383582dd21914e7ad1535fab72bc 100644 (file)
@@ -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;
index 13ca351eab0d4c99e1e4b79682319379cbbbcff3..49b0d4ad77f7f999ae213f6ae8db31f9036e2f02 100644 (file)
@@ -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));
        }
@@ -237,12 +417,6 @@ public class SshAgentClient implements SshAgent {
                return !closed.get();
        }
 
-       @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();
index 876a9999a2ad34f80d5639c84d299c73e12a507f..1b420e9d86d5f4cb9ef1ecfc9814e37e9752786d 100644 (file)
@@ -649,4 +649,61 @@ public class OpenSshConfigFileTest extends RepositoryTestCase {
                assertNotNull(h);
                assertPort(22, h);
        }
+
+       @Test
+       public void testTimeSpec() throws Exception {
+               assertEquals(-1, OpenSshConfigFile.timeSpec(null));
+               assertEquals(-1, OpenSshConfigFile.timeSpec(""));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("  "));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("s"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("  s"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec(" +s"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec(" -s"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("1ms"));
+               assertEquals(600, OpenSshConfigFile.timeSpec("600"));
+               assertEquals(600, OpenSshConfigFile.timeSpec("600s"));
+               assertEquals(600, OpenSshConfigFile.timeSpec("  600s"));
+               assertEquals(600, OpenSshConfigFile.timeSpec("  600s  "));
+               assertEquals(600, OpenSshConfigFile.timeSpec("\t600s"));
+               assertEquals(600, OpenSshConfigFile.timeSpec(" \t600  "));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("  600 s  "));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("600 s"));
+               assertEquals(600, OpenSshConfigFile.timeSpec("10m"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("1h30m"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("1h 30m"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("1h \t30m"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("1h+30m"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("1h +30m"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("1h + 30m"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("1h -30m"));
+               assertEquals(3630, OpenSshConfigFile.timeSpec("1h30s"));
+               assertEquals(5400, OpenSshConfigFile.timeSpec("30m 1h"));
+               assertEquals(3600, OpenSshConfigFile.timeSpec("30m 30m"));
+               assertEquals(60, OpenSshConfigFile.timeSpec("30 30"));
+               assertEquals(0, OpenSshConfigFile.timeSpec("0"));
+               assertEquals(1, OpenSshConfigFile.timeSpec("1"));
+               assertEquals(1, OpenSshConfigFile.timeSpec("1S"));
+               assertEquals(1, OpenSshConfigFile.timeSpec("1s"));
+               assertEquals(60, OpenSshConfigFile.timeSpec("1M"));
+               assertEquals(60, OpenSshConfigFile.timeSpec("1m"));
+               assertEquals(3600, OpenSshConfigFile.timeSpec("1H"));
+               assertEquals(3600, OpenSshConfigFile.timeSpec("1h"));
+               assertEquals(86400, OpenSshConfigFile.timeSpec("1D"));
+               assertEquals(86400, OpenSshConfigFile.timeSpec("1d"));
+               assertEquals(604800, OpenSshConfigFile.timeSpec("1W"));
+               assertEquals(604800, OpenSshConfigFile.timeSpec("1w"));
+               assertEquals(172800, OpenSshConfigFile.timeSpec("2d"));
+               assertEquals(604800, OpenSshConfigFile.timeSpec("1w"));
+               assertEquals(604800 + 172800 + 3 * 3600 + 30 * 60 + 10,
+                               OpenSshConfigFile.timeSpec("1w2d3h30m10s"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("-7"));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("-9d"));
+               assertEquals(Integer.MAX_VALUE, OpenSshConfigFile
+                               .timeSpec(Integer.toString(Integer.MAX_VALUE)));
+               assertEquals(-1, OpenSshConfigFile
+                               .timeSpec(Long.toString(Integer.MAX_VALUE + 1L)));
+               assertEquals(-1, OpenSshConfigFile
+                               .timeSpec(Integer.toString(Integer.MAX_VALUE / 60 + 1) + 'M'));
+               assertEquals(-1, OpenSshConfigFile.timeSpec("1000000000000000000000w"));
+       }
 }
index cf966a528e75d945947d48e08f5ce1cd6daabf9d..659ccb8c55fa267820e6260464142669b643dee2 100644 (file)
@@ -501,6 +501,98 @@ public class OpenSshConfigFile implements SshConfigStore {
                                || SshConstants.TRUE.equals(value);
        }
 
+       /**
+        * Converts an OpenSSH time value into a number of seconds. The format is
+        * defined by OpenSSH as a sequence of (positive) integers with suffixes for
+        * seconds, minutes, hours, days, and weeks.
+        *
+        * @param value
+        *            to convert
+        * @return the parsed value as a number of seconds, or -1 if the value is
+        *         not a valid OpenSSH time value
+        * @see <a href="https://man.openbsd.org/sshd_config.5#TIME_FORMATS">OpenBSD
+        *      man 5 sshd_config, section TIME FORMATS</a>
+        */
+       public static int timeSpec(String value) {
+               if (value == null) {
+                       return -1;
+               }
+               try {
+                       int length = value.length();
+                       int i = 0;
+                       int seconds = 0;
+                       boolean valueSeen = false;
+                       while (i < length) {
+                               // Skip whitespace
+                               char ch = value.charAt(i);
+                               if (Character.isWhitespace(ch)) {
+                                       i++;
+                                       continue;
+                               }
+                               if (ch == '+') {
+                                       // OpenSSH uses strtol with base 10: a leading plus sign is
+                                       // allowed.
+                                       i++;
+                               }
+                               int val = 0;
+                               int j = i;
+                               while (j < length) {
+                                       ch = value.charAt(j++);
+                                       if (ch >= '0' && ch <= '9') {
+                                               val = Math.addExact(Math.multiplyExact(val, 10),
+                                                               ch - '0');
+                                       } else {
+                                               j--;
+                                               break;
+                                       }
+                               }
+                               if (i == j) {
+                                       // No digits seen
+                                       return -1;
+                               }
+                               i = j;
+                               int multiplier = 1;
+                               if (i < length) {
+                                       ch = value.charAt(i++);
+                                       switch (ch) {
+                                       case 's':
+                                       case 'S':
+                                               break;
+                                       case 'm':
+                                       case 'M':
+                                               multiplier = 60;
+                                               break;
+                                       case 'h':
+                                       case 'H':
+                                               multiplier = 3600;
+                                               break;
+                                       case 'd':
+                                       case 'D':
+                                               multiplier = 24 * 3600;
+                                               break;
+                                       case 'w':
+                                       case 'W':
+                                               multiplier = 7 * 24 * 3600;
+                                               break;
+                                       default:
+                                               if (Character.isWhitespace(ch)) {
+                                                       break;
+                                               }
+                                               // Invalid time spec
+                                               return -1;
+                                       }
+                               }
+                               seconds = Math.addExact(seconds,
+                                               Math.multiplyExact(val, multiplier));
+                               valueSeen = true;
+                       }
+                       return valueSeen ? seconds : -1;
+               } catch (ArithmeticException e) {
+                       // Overflow
+                       return -1;
+               }
+       }
+
        /**
         * Retrieves the local user name as given in the constructor.
         *
@@ -549,6 +641,7 @@ public class OpenSshConfigFile implements SshConfigStore {
                        LIST_KEYS.add(SshConstants.GLOBAL_KNOWN_HOSTS_FILE);
                        LIST_KEYS.add(SshConstants.SEND_ENV);
                        LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
+                       LIST_KEYS.add(SshConstants.ADD_KEYS_TO_AGENT); // confirm timeSpec
                }
 
                /**
index 698982e1ae85be2e01887d8af85fea7849821072..d6bdbd800daa2e6aed2b3be02902c0604603bf08 100644 (file)
@@ -44,6 +44,14 @@ public final class SshConstants {
 
        // Config file keys
 
+       /**
+        * Property to control whether private keys are added to an SSH agent, if
+        * one is running, after having been loaded.
+        *
+        * @since 6.1
+        */
+       public static final String ADD_KEYS_TO_AGENT = "AddKeysToAgent";
+
        /** Key in an ssh config file. */
        public static final String BATCH_MODE = "BatchMode";
 
@@ -159,6 +167,14 @@ public final class SshConstants {
        /** Key in an ssh config file. */
        public static final String REMOTE_FORWARD = "RemoteForward";
 
+       /**
+        * (Absolute) path to a middleware library the SSH agent shall use to load
+        * SK (U2F) keys.
+        *
+        * @since 6.1
+        */
+       public static final String SECURITY_KEY_PROVIDER = "SecurityKeyProvider";
+
        /** Key in an ssh config file. */
        public static final String SEND_ENV = "SendEnv";