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)",
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)",
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})
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
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;
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;
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;
/**
private HostConfigEntry hostConfig;
+ private boolean addKeysToAgent;
+
+ private boolean askBeforeAdding;
+
+ private String skProvider;
+
+ private SshAgentKeyConstraint[] constraints;
+
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
}
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$
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 {
};
}
}
-
- @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;
- }
-
}
/***/ public String proxySocksUnexpectedMessage;
/***/ public String proxySocksUnexpectedVersion;
/***/ public String proxySocksUsernameTooLong;
+ /***/ public String pubkeyAuthAddKeyToAgentError;
+ /***/ public String pubkeyAuthAddKeyToAgentQuestion;
/***/ public String pubkeyAuthWrongCommand;
/***/ public String pubkeyAuthWrongKey;
/***/ public String pubkeyAuthWrongSignatureAlgorithm;
/***/ public String serverIdWithNul;
/***/ public String sessionCloseFailed;
/***/ public String sessionWithoutUsername;
+ /***/ public String sshAgentEdDSAFormatError;
+ /***/ public String sshAgentPayloadLengthError;
/***/ public String sshAgentReplyLengthError;
/***/ public String sshAgentReplyUnexpected;
/***/ public String sshAgentShortReadBuffer;
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;
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>
}
}
+ @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));
}
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();
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"));
+ }
}
|| 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.
*
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
}
/**
// 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";
/** 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";