sshd: store per-session data on the sshd session object

Don't store session properties on the client but in a dedicated
per-session object that is attached to the sshd session.

Also make sure that each sshd session gets its own instance of
IdentityPasswordProvider that asks for passphrases of encrypted
private keys, and also store it on the session itself.

Bug: 563380
Change-Id: Ia88bf9f91cd22b5fd32b5972d8204d60f2de56bf
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2020-07-25 10:20:29 +02:00
parent 0b487b4fcd
commit 76f79bc36c
5 changed files with 225 additions and 95 deletions

View File

@ -18,16 +18,22 @@
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import org.apache.sshd.client.ClientFactoryManager; import org.apache.sshd.client.ClientFactoryManager;
import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.keyverifier.ServerKeyVerifier; import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSessionImpl; import org.apache.sshd.client.session.ClientSessionImpl;
import org.apache.sshd.common.AttributeRepository;
import org.apache.sshd.common.FactoryManager; import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.PropertyResolver;
import org.apache.sshd.common.PropertyResolverUtils; import org.apache.sshd.common.PropertyResolverUtils;
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;
@ -419,4 +425,122 @@ private static String escapeControls(String s) {
return b.toString(); return b.toString();
} }
@Override
public <T> T getAttribute(AttributeKey<T> key) {
T value = super.getAttribute(key);
if (value == null) {
IoSession ioSession = getIoSession();
if (ioSession != null) {
Object obj = ioSession.getAttribute(AttributeRepository.class);
if (obj instanceof AttributeRepository) {
AttributeRepository sessionAttributes = (AttributeRepository) obj;
value = sessionAttributes.resolveAttribute(key);
}
}
}
return value;
}
@Override
public PropertyResolver getParentPropertyResolver() {
IoSession ioSession = getIoSession();
if (ioSession != null) {
Object obj = ioSession.getAttribute(AttributeRepository.class);
if (obj instanceof PropertyResolver) {
return (PropertyResolver) obj;
}
}
return super.getParentPropertyResolver();
}
/**
* An {@link AttributeRepository} that chains together two other attribute
* sources in a hierarchy.
*/
public static class ChainingAttributes implements AttributeRepository {
private final AttributeRepository delegate;
private final AttributeRepository parent;
/**
* Create a new {@link ChainingAttributes} attribute source.
*
* @param self
* to search for attributes first
* @param parent
* to search for attributes if not found in {@code self}
*/
public ChainingAttributes(AttributeRepository self,
AttributeRepository parent) {
this.delegate = self;
this.parent = parent;
}
@Override
public int getAttributesCount() {
return delegate.getAttributesCount();
}
@Override
public <T> T getAttribute(AttributeKey<T> key) {
return delegate.getAttribute(Objects.requireNonNull(key));
}
@Override
public Collection<AttributeKey<?>> attributeKeys() {
return delegate.attributeKeys();
}
@Override
public <T> T resolveAttribute(AttributeKey<T> key) {
T value = getAttribute(Objects.requireNonNull(key));
if (value == null) {
return parent.getAttribute(key);
}
return value;
}
}
/**
* A {@link ChainingAttributes} repository that doubles as a
* {@link PropertyResolver}. The property map can be set via the attribute
* key {@link SessionAttributes#PROPERTIES}.
*/
public static class SessionAttributes extends ChainingAttributes
implements PropertyResolver {
/** Key for storing a map of properties in the attributes. */
public static final AttributeKey<Map<String, Object>> PROPERTIES = new AttributeKey<>();
private final PropertyResolver parentProperties;
/**
* Creates a new {@link SessionAttributes} attribute and property
* source.
*
* @param self
* to search for attributes first
* @param parent
* to search for attributes if not found in {@code self}
* @param parentProperties
* to search for properties if not found in {@code self}
*/
public SessionAttributes(AttributeRepository self,
AttributeRepository parent, PropertyResolver parentProperties) {
super(self, parent);
this.parentProperties = parentProperties;
}
@Override
public PropertyResolver getParentPropertyResolver() {
return parentProperties;
}
@Override
public Map<String, Object> getProperties() {
Map<String, Object> props = getAttribute(PROPERTIES);
return props == null ? Collections.emptyMap() : props;
}
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -23,12 +23,16 @@
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.sshd.client.ClientAuthenticationManager;
import org.apache.sshd.client.SshClient; import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.future.ConnectFuture; import org.apache.sshd.client.future.ConnectFuture;
@ -45,6 +49,8 @@
import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.session.helpers.AbstractSession; import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.ValidateUtils;
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.ChainingAttributes;
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.SessionAttributes;
import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector; import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector;
import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector; import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
@ -52,6 +58,7 @@
import org.eclipse.jgit.transport.sshd.KeyCache; import org.eclipse.jgit.transport.sshd.KeyCache;
import org.eclipse.jgit.transport.sshd.ProxyData; import org.eclipse.jgit.transport.sshd.ProxyData;
import org.eclipse.jgit.transport.sshd.ProxyDataFactory; import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
import org.eclipse.jgit.util.StringUtils;
/** /**
* Customized {@link SshClient} for JGit. It creates specialized * Customized {@link SshClient} for JGit. It creates specialized
@ -100,35 +107,55 @@ public ConnectFuture connect(HostConfigEntry hostConfig,
int port = hostConfig.getPort(); int port = hostConfig.getPort();
ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port); //$NON-NLS-1$ ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port); //$NON-NLS-1$
String userName = hostConfig.getUsername(); String userName = hostConfig.getUsername();
AttributeRepository attributes = chain(context, this);
InetSocketAddress address = new InetSocketAddress(host, port); InetSocketAddress address = new InetSocketAddress(host, port);
ConnectFuture connectFuture = new DefaultConnectFuture( ConnectFuture connectFuture = new DefaultConnectFuture(
userName + '@' + address, null); userName + '@' + address, null);
SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener( SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener(
connectFuture, userName, address, hostConfig); connectFuture, userName, address, hostConfig);
// sshd needs some entries from the host config already in the attributes = sessionAttributes(attributes, hostConfig, address);
// constructor of the session. Put those as properties on this client,
// where it will find them. We can set the host config only once the
// session object has been created.
copyProperty(
hostConfig.getProperty(SshConstants.PREFERRED_AUTHENTICATIONS,
getAttribute(PREFERRED_AUTHENTICATIONS)),
PREFERRED_AUTHS);
setAttribute(HOST_CONFIG_ENTRY, hostConfig);
setAttribute(ORIGINAL_REMOTE_ADDRESS, address);
// Proxy support // Proxy support
ProxyData proxy = getProxyData(address); ProxyData proxy = getProxyData(address);
if (proxy != null) { if (proxy != null) {
address = configureProxy(proxy, address); address = configureProxy(proxy, address);
proxy.clearPassword(); proxy.clearPassword();
} }
connector.connect(address, this, localAddress).addListener(listener); connector.connect(address, attributes, localAddress)
.addListener(listener);
return connectFuture; return connectFuture;
} }
private void copyProperty(String value, String key) { private AttributeRepository chain(AttributeRepository self,
if (value != null && !value.isEmpty()) { AttributeRepository parent) {
getProperties().put(key, value); if (self == null) {
return Objects.requireNonNull(parent);
} }
if (parent == null || parent == self) {
return self;
}
return new ChainingAttributes(self, parent);
}
private AttributeRepository sessionAttributes(AttributeRepository parent,
HostConfigEntry hostConfig, InetSocketAddress originalAddress) {
// sshd needs some entries from the host config already in the
// constructor of the session. Put those into a dedicated
// AttributeRepository for the new session where it will find them.
// We can set the host config only once the session object has been
// created.
Map<AttributeKey<?>, Object> data = new HashMap<>();
data.put(HOST_CONFIG_ENTRY, hostConfig);
data.put(ORIGINAL_REMOTE_ADDRESS, originalAddress);
String preferredAuths = hostConfig.getProperty(
SshConstants.PREFERRED_AUTHENTICATIONS,
resolveAttribute(PREFERRED_AUTHENTICATIONS));
if (!StringUtils.isEmptyOrNull(preferredAuths)) {
data.put(SessionAttributes.PROPERTIES,
Collections.singletonMap(PREFERRED_AUTHS, preferredAuths));
}
return new SessionAttributes(
AttributeRepository.ofAttributesMap(data),
parent, this);
} }
private ProxyData getProxyData(InetSocketAddress remoteAddress) { private ProxyData getProxyData(InetSocketAddress remoteAddress) {
@ -219,11 +246,6 @@ private JGitClientSession createSession(IoSession ioSession,
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig); int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
session.getProperties().put(PASSWORD_PROMPTS, session.getProperties().put(PASSWORD_PROMPTS,
Integer.valueOf(numberOfPasswordPrompts)); Integer.valueOf(numberOfPasswordPrompts));
FilePasswordProvider passwordProvider = getFilePasswordProvider();
if (passwordProvider instanceof RepeatingFilePasswordProvider) {
((RepeatingFilePasswordProvider) passwordProvider)
.setAttempts(numberOfPasswordPrompts);
}
List<Path> identities = hostConfig.getIdentities().stream() List<Path> identities = hostConfig.getIdentities().stream()
.map(s -> { .map(s -> {
try { try {
@ -237,6 +259,7 @@ private JGitClientSession createSession(IoSession ioSession,
.collect(Collectors.toList()); .collect(Collectors.toList());
CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider( CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider(
identities, keyCache); identities, keyCache);
FilePasswordProvider passwordProvider = getFilePasswordProvider();
ourConfiguredKeysProvider.setPasswordFinder(passwordProvider); ourConfiguredKeysProvider.setPasswordFinder(passwordProvider);
if (hostConfig.isIdentitiesOnly()) { if (hostConfig.isIdentitiesOnly()) {
session.setKeyIdentityProvider(ourConfiguredKeysProvider); session.setKeyIdentityProvider(ourConfiguredKeysProvider);
@ -265,9 +288,7 @@ private int getNumberOfPasswordPrompts(HostConfigEntry hostConfig) {
log.warn(format(SshdText.get().configInvalidPositive, log.warn(format(SshdText.get().configInvalidPositive,
SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts)); SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
} }
// Default for NumberOfPasswordPrompts according to return ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS;
// https://man.openbsd.org/ssh_config
return 3;
} }
/** /**
@ -408,6 +429,5 @@ public KeyPair next() {
}; };
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -16,8 +16,12 @@
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.apache.sshd.client.ClientAuthenticationManager;
import org.apache.sshd.common.AttributeRepository.AttributeKey;
import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.session.SessionContext; import org.apache.sshd.common.session.SessionContext;
import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.CredentialsProvider;
@ -25,39 +29,61 @@
import org.eclipse.jgit.transport.sshd.KeyPasswordProvider; import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
/** /**
* A bridge from sshd's {@link RepeatingFilePasswordProvider} to our * A bridge from sshd's {@link FilePasswordProvider} to our per-session
* {@link KeyPasswordProvider} API. * {@link KeyPasswordProvider} API.
*/ */
public class PasswordProviderWrapper implements RepeatingFilePasswordProvider { public class PasswordProviderWrapper implements FilePasswordProvider {
private final KeyPasswordProvider delegate; private static final AttributeKey<PerSessionState> STATE = new AttributeKey<>();
private Map<String, AtomicInteger> counts = new ConcurrentHashMap<>(); private static class PerSessionState {
Map<String, AtomicInteger> counts = new ConcurrentHashMap<>();
KeyPasswordProvider delegate;
}
private final Supplier<KeyPasswordProvider> factory;
/** /**
* @param delegate * Creates a new {@link PasswordProviderWrapper}.
*
* @param factory
* to use to create per-session {@link KeyPasswordProvider}s
*/ */
public PasswordProviderWrapper(@NonNull KeyPasswordProvider delegate) { public PasswordProviderWrapper(
this.delegate = delegate; @NonNull Supplier<KeyPasswordProvider> factory) {
this.factory = factory;
} }
@Override private PerSessionState getState(SessionContext context) {
public void setAttempts(int numberOfPasswordPrompts) { PerSessionState state = context.getAttribute(STATE);
delegate.setAttempts(numberOfPasswordPrompts); if (state == null) {
} state = new PerSessionState();
state.delegate = factory.get();
@Override Integer maxNumberOfAttempts = context
public int getAttempts() { .getInteger(ClientAuthenticationManager.PASSWORD_PROMPTS);
return delegate.getAttempts(); if (maxNumberOfAttempts != null
&& maxNumberOfAttempts.intValue() > 0) {
state.delegate.setAttempts(maxNumberOfAttempts.intValue());
} else {
state.delegate.setAttempts(
ClientAuthenticationManager.DEFAULT_PASSWORD_PROMPTS);
}
context.setAttribute(STATE, state);
}
return state;
} }
@Override @Override
public String getPassword(SessionContext session, NamedResource resource, public String getPassword(SessionContext session, NamedResource resource,
int attemptIndex) throws IOException { int attemptIndex) throws IOException {
String key = resource.getName(); String key = resource.getName();
int attempt = counts PerSessionState state = getState(session);
int attempt = state.counts
.computeIfAbsent(key, k -> new AtomicInteger()).get(); .computeIfAbsent(key, k -> new AtomicInteger()).get();
char[] passphrase = delegate.getPassphrase(toUri(key), attempt); char[] passphrase = state.delegate.getPassphrase(toUri(key), attempt);
if (passphrase == null) { if (passphrase == null) {
return null; return null;
} }
@ -74,18 +100,19 @@ public ResourceDecodeResult handleDecodeAttemptResult(
String password, Exception err) String password, Exception err)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
String key = resource.getName(); String key = resource.getName();
AtomicInteger count = counts.get(key); PerSessionState state = getState(session);
AtomicInteger count = state.counts.get(key);
int numberOfAttempts = count == null ? 0 : count.incrementAndGet(); int numberOfAttempts = count == null ? 0 : count.incrementAndGet();
ResourceDecodeResult result = null; ResourceDecodeResult result = null;
try { try {
if (delegate.keyLoaded(toUri(key), numberOfAttempts, err)) { if (state.delegate.keyLoaded(toUri(key), numberOfAttempts, err)) {
result = ResourceDecodeResult.RETRY; result = ResourceDecodeResult.RETRY;
} else { } else {
result = ResourceDecodeResult.TERMINATE; result = ResourceDecodeResult.TERMINATE;
} }
} finally { } finally {
if (result != ResourceDecodeResult.RETRY) { if (result != ResourceDecodeResult.RETRY) {
counts.remove(key); state.counts.remove(key);
} }
} }
return result; return result;

View File

@ -1,41 +0,0 @@
/*
* Copyright (C) 2018, 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 org.apache.sshd.common.config.keys.FilePasswordProvider;
/**
* A {@link FilePasswordProvider} augmented to support repeatedly asking for
* passwords.
*
*/
public interface RepeatingFilePasswordProvider extends FilePasswordProvider {
/**
* Define the maximum number of attempts to get a password that should be
* attempted for one identity resource through this provider.
*
* @param numberOfPasswordPrompts
* number of times to ask for a password;
* {@link IllegalArgumentException} may be thrown if <= 0
*/
void setAttempts(int numberOfPasswordPrompts);
/**
* Gets the maximum number of attempts to get a password that should be
* attempted for one identity resource through this provider.
*
* @return the maximum number of attempts to try, always >= 1.
*/
default int getAttempts() {
return 1;
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch> and others * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
* *
* This program and the accompanying materials are made available under the * This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at * terms of the Eclipse Distribution License v. 1.0 which is available at
@ -25,6 +25,7 @@
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.sshd.client.ClientBuilder; import org.apache.sshd.client.ClientBuilder;
@ -194,12 +195,11 @@ public SshdSession getSession(URIish uri,
home, sshDir); home, sshDir);
KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider( KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
getDefaultKeys(sshDir)); getDefaultKeys(sshDir));
KeyPasswordProvider passphrases = createKeyPasswordProvider(
credentialsProvider);
SshClient client = ClientBuilder.builder() SshClient client = ClientBuilder.builder()
.factory(JGitSshClient::new) .factory(JGitSshClient::new)
.filePasswordProvider( .filePasswordProvider(createFilePasswordProvider(
createFilePasswordProvider(passphrases)) () -> createKeyPasswordProvider(
credentialsProvider)))
.hostConfigEntryResolver(configFile) .hostConfigEntryResolver(configFile)
.serverKeyVerifier(new JGitServerKeyVerifier( .serverKeyVerifier(new JGitServerKeyVerifier(
getServerKeyDatabase(home, sshDir))) getServerKeyDatabase(home, sshDir)))
@ -536,14 +536,14 @@ protected KeyPasswordProvider createKeyPasswordProvider(
/** /**
* Creates a {@link FilePasswordProvider} for a new session. * Creates a {@link FilePasswordProvider} for a new session.
* *
* @param provider * @param providerFactory
* the {@link KeyPasswordProvider} to delegate to * providing the {@link KeyPasswordProvider} to delegate to
* @return a new {@link FilePasswordProvider} * @return a new {@link FilePasswordProvider}
*/ */
@NonNull @NonNull
private FilePasswordProvider createFilePasswordProvider( private FilePasswordProvider createFilePasswordProvider(
KeyPasswordProvider provider) { Supplier<KeyPasswordProvider> providerFactory) {
return new PasswordProviderWrapper(provider); return new PasswordProviderWrapper(providerFactory);
} }
/** /**