diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF index 754944f2e..2ba94f205 100644 --- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF @@ -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)", 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 4b12db5d5..b529e5b5a 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 @@ -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 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 bfe11cb74..96da0cccd 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 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 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.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> factories) { super(factories); } @@ -95,9 +114,130 @@ protected Iterator createPublicKeyIterator( 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> ids = agent + .getIdentities(); + if (ids == null) { + return false; + } + Iterator> 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 @@ private SshAgent getAgent(ClientSession session) throws Exception { 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 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 KeyAgentIdentity next() { }; } } - - @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 f7b6f6aca..b8c94eea5 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 static SshdText get() { /***/ 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 static SshdText get() { /***/ 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 13ca351ea..49b0d4ad7 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 @@ 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.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 SSH * Agent Protocol, RFC draft @@ -224,6 +230,180 @@ public Map.Entry sign(SessionContext session, PublicKey key, } } + @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): + * + *
+	 * 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
+	 * }
+	 * 
+ *

+ * 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." + *

+ * + *
+	 * CurvePrivateKey ::= OCTET STRING
+	 * 
+ * + * @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 RFC 5958 + * @see RFC 8410 + */ + 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 boolean isOpen() { 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(); diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java index 876a9999a..1b420e9d8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFileTest.java @@ -649,4 +649,61 @@ public void testWhitespace() throws Exception { 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")); + } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java index cf966a528..659ccb8c5 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/ssh/OpenSshConfigFile.java @@ -501,6 +501,98 @@ public static boolean flag(String value) { || 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 OpenBSD + * man 5 sshd_config, section TIME FORMATS + */ + 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 static class HostEntry implements SshConfigStore.HostConfig { 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 } /** diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java index 698982e1a..d6bdbd800 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/SshConstants.java @@ -44,6 +44,14 @@ private 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 @@ private 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";