sshd: support the AddKeysToAgent ssh config
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>
This commit is contained in:
parent
68bd2c1462
commit
b73548bc4c
|
@ -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;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;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.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.digest;version="[2.8.0,2.9.0)",
|
||||||
org.apache.sshd.common.forward;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.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.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.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;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.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.io.resource;version="[2.8.0,2.9.0)",
|
||||||
org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)",
|
org.apache.sshd.common.util.logging;version="[2.8.0,2.9.0)",
|
||||||
|
|
|
@ -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}
|
proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2}
|
||||||
proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0}
|
proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0}
|
||||||
proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2}
|
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})
|
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})
|
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})
|
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}
|
serverIdWithNul=Server identification contains a NUL character: {0}
|
||||||
sessionCloseFailed=Closing the session failed
|
sessionCloseFailed=Closing the session failed
|
||||||
sessionWithoutUsername=SSH session created without user name; cannot authenticate
|
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}
|
sshAgentReplyLengthError=Invalid SSH agent reply message length {0} after command {1}
|
||||||
sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0}
|
sshAgentReplyUnexpected=Unexpected reply from ssh-agent: {0}
|
||||||
sshAgentShortReadBuffer=Short read from SSH agent
|
sshAgentShortReadBuffer=Short read from SSH agent
|
||||||
|
|
|
@ -13,13 +13,17 @@
|
||||||
import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
|
import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.InvalidPathException;
|
import java.nio.file.InvalidPathException;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyPair;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -30,6 +34,7 @@
|
||||||
|
|
||||||
import org.apache.sshd.agent.SshAgent;
|
import org.apache.sshd.agent.SshAgent;
|
||||||
import org.apache.sshd.agent.SshAgentFactory;
|
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.KeyAgentIdentity;
|
||||||
import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
|
import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
|
||||||
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
|
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.AuthorizedKeyEntry;
|
||||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
import org.apache.sshd.common.config.keys.KeyUtils;
|
||||||
import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
|
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.Signature;
|
||||||
import org.apache.sshd.common.signature.SignatureFactoriesManager;
|
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;
|
import org.eclipse.jgit.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,6 +66,14 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
|
||||||
|
|
||||||
private HostConfigEntry hostConfig;
|
private HostConfigEntry hostConfig;
|
||||||
|
|
||||||
|
private boolean addKeysToAgent;
|
||||||
|
|
||||||
|
private boolean askBeforeAdding;
|
||||||
|
|
||||||
|
private String skProvider;
|
||||||
|
|
||||||
|
private SshAgentKeyConstraint[] constraints;
|
||||||
|
|
||||||
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
|
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
|
||||||
super(factories);
|
super(factories);
|
||||||
}
|
}
|
||||||
|
@ -95,9 +114,130 @@ protected Iterator<PublicKeyIdentity> createPublicKeyIterator(
|
||||||
ClientSession session, SignatureFactoriesManager manager)
|
ClientSession session, SignatureFactoriesManager manager)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
agent = getAgent(session);
|
agent = getAgent(session);
|
||||||
|
if (agent != null) {
|
||||||
|
parseAddKeys(hostConfig);
|
||||||
|
if (addKeysToAgent) {
|
||||||
|
skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER);
|
||||||
|
}
|
||||||
|
}
|
||||||
return new KeyIterator(session, manager);
|
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 {
|
private SshAgent getAgent(ClientSession session) throws Exception {
|
||||||
FactoryManager manager = Objects.requireNonNull(
|
FactoryManager manager = Objects.requireNonNull(
|
||||||
session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$
|
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);
|
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
|
@Override
|
||||||
protected void releaseKeys() throws IOException {
|
protected void releaseKeys() throws IOException {
|
||||||
|
addKeysToAgent = false;
|
||||||
|
askBeforeAdding = false;
|
||||||
|
skProvider = null;
|
||||||
|
constraints = null;
|
||||||
try {
|
try {
|
||||||
if (agent != null) {
|
if (agent != null) {
|
||||||
try {
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,8 @@ public static SshdText get() {
|
||||||
/***/ public String proxySocksUnexpectedMessage;
|
/***/ public String proxySocksUnexpectedMessage;
|
||||||
/***/ public String proxySocksUnexpectedVersion;
|
/***/ public String proxySocksUnexpectedVersion;
|
||||||
/***/ public String proxySocksUsernameTooLong;
|
/***/ public String proxySocksUsernameTooLong;
|
||||||
|
/***/ public String pubkeyAuthAddKeyToAgentError;
|
||||||
|
/***/ public String pubkeyAuthAddKeyToAgentQuestion;
|
||||||
/***/ public String pubkeyAuthWrongCommand;
|
/***/ public String pubkeyAuthWrongCommand;
|
||||||
/***/ public String pubkeyAuthWrongKey;
|
/***/ public String pubkeyAuthWrongKey;
|
||||||
/***/ public String pubkeyAuthWrongSignatureAlgorithm;
|
/***/ public String pubkeyAuthWrongSignatureAlgorithm;
|
||||||
|
@ -107,6 +109,8 @@ public static SshdText get() {
|
||||||
/***/ public String serverIdWithNul;
|
/***/ public String serverIdWithNul;
|
||||||
/***/ public String sessionCloseFailed;
|
/***/ public String sessionCloseFailed;
|
||||||
/***/ public String sessionWithoutUsername;
|
/***/ public String sessionWithoutUsername;
|
||||||
|
/***/ public String sshAgentEdDSAFormatError;
|
||||||
|
/***/ public String sshAgentPayloadLengthError;
|
||||||
/***/ public String sshAgentReplyLengthError;
|
/***/ public String sshAgentReplyLengthError;
|
||||||
/***/ public String sshAgentReplyUnexpected;
|
/***/ public String sshAgentReplyUnexpected;
|
||||||
/***/ public String sshAgentShortReadBuffer;
|
/***/ public String sshAgentShortReadBuffer;
|
||||||
|
|
|
@ -11,10 +11,12 @@
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
|
import java.security.PrivateKey;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -25,19 +27,23 @@
|
||||||
import org.apache.sshd.agent.SshAgentKeyConstraint;
|
import org.apache.sshd.agent.SshAgentKeyConstraint;
|
||||||
import org.apache.sshd.common.SshException;
|
import org.apache.sshd.common.SshException;
|
||||||
import org.apache.sshd.common.config.keys.KeyUtils;
|
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.session.SessionContext;
|
||||||
import org.apache.sshd.common.util.buffer.Buffer;
|
import org.apache.sshd.common.util.buffer.Buffer;
|
||||||
import org.apache.sshd.common.util.buffer.BufferException;
|
import org.apache.sshd.common.util.buffer.BufferException;
|
||||||
import org.apache.sshd.common.util.buffer.BufferUtils;
|
import org.apache.sshd.common.util.buffer.BufferUtils;
|
||||||
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
|
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.internal.transport.sshd.SshdText;
|
||||||
import org.eclipse.jgit.transport.sshd.agent.Connector;
|
import org.eclipse.jgit.transport.sshd.agent.Connector;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A client for an SSH2 agent. This client supports only querying identities and
|
* A client for an SSH2 agent. This client supports querying identities,
|
||||||
* signature requests.
|
* 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
|
* @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH
|
||||||
* Agent Protocol, RFC draft</a>
|
* Agent Protocol, RFC draft</a>
|
||||||
|
@ -224,6 +230,180 @@ public Map.Entry<String, byte[]> 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):
|
||||||
|
*
|
||||||
|
* <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 {
|
private Buffer rpc(byte command, byte[] message) throws IOException {
|
||||||
return new ByteArrayBuffer(connector.rpc(command, message));
|
return new ByteArrayBuffer(connector.rpc(command, message));
|
||||||
}
|
}
|
||||||
|
@ -237,12 +417,6 @@ public boolean isOpen() {
|
||||||
return !closed.get();
|
return !closed.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addIdentity(KeyPair key, String comment,
|
|
||||||
SshAgentKeyConstraint... constraints) throws IOException {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeIdentity(PublicKey key) throws IOException {
|
public void removeIdentity(PublicKey key) throws IOException {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
|
@ -649,4 +649,61 @@ public void testWhitespace() throws Exception {
|
||||||
assertNotNull(h);
|
assertNotNull(h);
|
||||||
assertPort(22, 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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -501,6 +501,98 @@ public static boolean flag(String value) {
|
||||||
|| SshConstants.TRUE.equals(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 <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.
|
* 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.GLOBAL_KNOWN_HOSTS_FILE);
|
||||||
LIST_KEYS.add(SshConstants.SEND_ENV);
|
LIST_KEYS.add(SshConstants.SEND_ENV);
|
||||||
LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
|
LIST_KEYS.add(SshConstants.USER_KNOWN_HOSTS_FILE);
|
||||||
|
LIST_KEYS.add(SshConstants.ADD_KEYS_TO_AGENT); // confirm timeSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -44,6 +44,14 @@ private SshConstants() {
|
||||||
|
|
||||||
// Config file keys
|
// 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. */
|
/** Key in an ssh config file. */
|
||||||
public static final String BATCH_MODE = "BatchMode";
|
public static final String BATCH_MODE = "BatchMode";
|
||||||
|
|
||||||
|
@ -159,6 +167,14 @@ private SshConstants() {
|
||||||
/** Key in an ssh config file. */
|
/** Key in an ssh config file. */
|
||||||
public static final String REMOTE_FORWARD = "RemoteForward";
|
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. */
|
/** Key in an ssh config file. */
|
||||||
public static final String SEND_ENV = "SendEnv";
|
public static final String SEND_ENV = "SendEnv";
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue