blob: e1036c628332fc63742f811193089e375749af3d [file] [log] [blame]
/*
* Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.internal.transport.sshd;
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;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
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;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.FactoryManager;
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;
/**
* Custom {@link UserAuthPublicKey} implementation for handling SSH config
* PubkeyAcceptedAlgorithms and interaction with the SSH agent.
*/
public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
private SshAgent agent;
private HostConfigEntry hostConfig;
private boolean addKeysToAgent;
private boolean askBeforeAdding;
private String skProvider;
private SshAgentKeyConstraint[] constraints;
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
}
@Override
public void init(ClientSession rawSession, String service)
throws Exception {
if (!(rawSession instanceof JGitClientSession)) {
throw new IllegalStateException("Wrong session type: " //$NON-NLS-1$
+ rawSession.getClass().getCanonicalName());
}
JGitClientSession session = (JGitClientSession) rawSession;
hostConfig = session.getHostConfigEntry();
// Set signature algorithms for public key authentication
String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS);
if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
List<String> signatures = session.getSignatureFactoriesNames();
signatures = session.modifyAlgorithmList(signatures,
session.getAllAvailableSignatureAlgorithms(), pubkeyAlgos,
PUBKEY_ACCEPTED_ALGORITHMS);
if (!signatures.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures);
}
setSignatureFactoriesNames(signatures);
super.init(session, service);
return;
}
log.warn(format(SshdText.get().configNoKnownAlgorithms,
PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
}
// TODO: remove this once we're on an sshd version that has SSHD-1272
// fixed
List<NamedFactory<Signature>> localFactories = getSignatureFactories();
if (localFactories == null || localFactories.isEmpty()) {
setSignatureFactoriesNames(session.getSignatureFactoriesNames());
}
super.init(session, service);
}
@Override
protected Iterator<PublicKeyIdentity> 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<? 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$
SshAgentFactory factory = manager.getAgentFactory();
if (factory == null) {
return null;
}
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 {
agent.close();
} finally {
agent = null;
}
}
} finally {
super.releaseKeys();
}
}
private class KeyIterator extends UserAuthPublicKeyIterator {
private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys;
// If non-null, all the public keys from explicitly given key files. Any
// agent key not matching one of these public keys will be ignored in
// getIdentities().
private Collection<PublicKey> identityFiles;
public KeyIterator(ClientSession session,
SignatureFactoriesManager manager)
throws Exception {
super(session, manager);
}
private List<PublicKey> getExplicitKeys(
Collection<String> explicitFiles) {
if (explicitFiles == null) {
return null;
}
return explicitFiles.stream().map(s -> {
try {
Path p = Paths.get(s + ".pub"); //$NON-NLS-1$
if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) {
return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0)
.resolvePublicKey(null,
PublicKeyEntryResolver.IGNORING);
}
} catch (InvalidPathException | IOException
| GeneralSecurityException e) {
log.warn(format(SshdText.get().cannotReadPublicKey, s), e);
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList());
}
@Override
protected Iterable<KeyAgentIdentity> initializeAgentIdentities(
ClientSession session) throws IOException {
if (agent == null) {
return null;
}
agentKeys = agent.getIdentities();
if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
identityFiles = getExplicitKeys(hostConfig.getIdentities());
}
return () -> new Iterator<>() {
private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
.iterator();
private Map.Entry<PublicKey, String> next;
@Override
public boolean hasNext() {
while (next == null && iter.hasNext()) {
Map.Entry<PublicKey, String> val = iter.next();
PublicKey pk = val.getKey();
// This checks against all explicit keys for any agent
// key, but since identityFiles.size() is typically 1,
// it should be fine.
if (identityFiles == null || identityFiles.stream()
.anyMatch(k -> KeyUtils.compareKeys(k, pk))) {
next = val;
return true;
}
if (log.isTraceEnabled()) {
log.trace(
"Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$
KeyUtils.getKeyType(pk),
KeyUtils.getFingerPrint(pk));
}
}
return next != null;
}
@Override
public KeyAgentIdentity next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
KeyAgentIdentity result = new KeyAgentIdentity(agent,
next.getKey(), next.getValue());
next = null;
return result;
}
};
}
}
}