This is an automated email from the ASF dual-hosted git repository. twolf pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push: new b08a90a [SSHD-1141] Fix client-side server-sig-algs handling b08a90a is described below commit b08a90a5a3f68df592b39ce977028bf5d9211db6 Author: Thomas Wolf <tw...@apache.org> AuthorDate: Tue Mar 23 12:34:11 2021 +0100 [SSHD-1141] Fix client-side server-sig-algs handling Unconditionally announce that the client wants to get the server's SSH_MSG_EXT_INFO. Otherwise the server will never send it. Add the "ext-info-c" marker only on the very first key exchange proposal, and make sure the client doesn't send by mistake "ext-info-s". KexExtensions are available in all phases except PREKEX. When server-sig-algs is received, reorder the client session's signature factories such that algorithms the server announced as supported come first, followed by those not announced, both in client order. The client determines the order and the server just says what it supports. Note that per RFC 8308 [1] it's possible that a server doesn't announce _all_ the algorithms it supports, and a client is allowed to try unsupported algorithms, but may face authentication penalties such as back-off delays, authentication failures, or disconnections. [1] https://tools.ietf.org/html/rfc8308 --- .../DefaultClientKexExtensionHandler.java | 283 +++++---------------- 1 file changed, 69 insertions(+), 214 deletions(-) diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java index 09690a8..9085def 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/extension/DefaultClientKexExtensionHandler.java @@ -22,28 +22,19 @@ package org.apache.sshd.common.kex.extension; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NavigableSet; -import java.util.Objects; -import java.util.stream.Stream; +import java.util.Set; +import java.util.TreeSet; import org.apache.sshd.common.AttributeRepository.AttributeKey; import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.OptionalFeature; -import org.apache.sshd.common.config.keys.KeyUtils; import org.apache.sshd.common.kex.KexProposalOption; import org.apache.sshd.common.kex.extension.parser.ServerSignatureAlgorithms; import org.apache.sshd.common.session.Session; -import org.apache.sshd.common.signature.BuiltinSignatures; import org.apache.sshd.common.signature.Signature; -import org.apache.sshd.common.signature.SignatureFactory; import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.common.util.MapEntryUtils; -import org.apache.sshd.common.util.functors.UnaryEquator; import org.apache.sshd.common.util.logging.AbstractLoggingBean; /** @@ -52,27 +43,17 @@ import org.apache.sshd.common.util.logging.AbstractLoggingBean; * session by adding the <A HREF="https://tools.ietf.org/html/rfc8332">"rsa-sha2-256/512"</A> signature * factories (if not already added). * - * <B>Note:</B> experimental - used for development purposes and as an example - * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public class DefaultClientKexExtensionHandler extends AbstractLoggingBean implements KexExtensionHandler { - /** - * Session {@link AttributeKey} used to store the client's proposal - */ - public static final AttributeKey<Map<KexProposalOption, String>> CLIENT_PROPOSAL_KEY = new AttributeKey<>(); + + /** Default singleton instance. */ + public static final DefaultClientKexExtensionHandler INSTANCE = new DefaultClientKexExtensionHandler(); /** - * Session {@link AttributeKey} used to store the server's proposal + * Session {@link AttributeKey} used to store whether the extension indicator was already sent. */ - public static final AttributeKey<Map<KexProposalOption, String>> SERVER_PROPOSAL_KEY = new AttributeKey<>(); - - public static final NavigableSet<String> DEFAULT_EXTRA_SIGNATURES = Collections.unmodifiableNavigableSet( - GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, - KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS, - KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS)); - - public static final DefaultClientKexExtensionHandler INSTANCE = new DefaultClientKexExtensionHandler(); + public static final AttributeKey<Boolean> CLIENT_PROPOSAL_MADE = new AttributeKey<>(); public DefaultClientKexExtensionHandler() { super(); @@ -80,219 +61,93 @@ public class DefaultClientKexExtensionHandler extends AbstractLoggingBean implem @Override public boolean isKexExtensionsAvailable(Session session, AvailabilityPhase phase) throws IOException { - if ((session == null) || session.isServerSession()) { - return false; - } - - // We only need to take special care during the proposal build phase - if (phase != AvailabilityPhase.PROPOSAL) { - return true; - } - - boolean debugEnabled = log.isDebugEnabled(); - // Check if client already sent its proposal - if not, we can still influence it - Map<KexProposalOption, String> clientProposal = session.getAttribute(CLIENT_PROPOSAL_KEY); - Map<KexProposalOption, String> serverProposal = session.getAttribute(SERVER_PROPOSAL_KEY); - if (MapEntryUtils.isNotEmpty(clientProposal)) { - if (debugEnabled) { - log.debug("isKexExtensionsAvailable({})[{}] already sent proposal={} (server={})", - session, phase, clientProposal, serverProposal); - } - return false; - } - - /* - * According to https://tools.ietf.org/html/rfc8308#section-3.1: - * - * - * Note that implementations are known to exist that apply authentication penalties if the client attempts to - * use an unexpected public key algorithm. - * - * Therefore we want to be sure the server declared its support for extensions before we declare ours. - */ - if (MapEntryUtils.isEmpty(serverProposal)) { - if (debugEnabled) { - log.debug("isKexExtensionsAvailable({})[{}] no server proposal", session, phase); - } - return false; - } - - String algos = serverProposal.get(KexProposalOption.ALGORITHMS); - String extDeclared = Stream.of(GenericUtils.split(algos, ',')) - .filter(s -> KexExtensions.SERVER_KEX_EXTENSION.equalsIgnoreCase(s)) - .findFirst() - .orElse(null); - if (GenericUtils.isEmpty(extDeclared)) { - if (debugEnabled) { - log.debug("isKexExtensionsAvailable({})[{}] server proposal does not include extension indicator: {}", - session, phase, algos); - } - return false; - } - - return true; + return !AvailabilityPhase.PREKEX.equals(phase); } @Override public void handleKexInitProposal( Session session, boolean initiator, Map<KexProposalOption, String> proposal) throws IOException { - if (session.isServerSession()) { - return; // just in case + // If it's the very first time, we may add the marker telling the server that we are ready to + // handle SSH_MSG_EXT_INFO. + if (session == null || session.isServerSession() || !initiator) { + return; } - - session.setAttribute(initiator ? CLIENT_PROPOSAL_KEY : SERVER_PROPOSAL_KEY, new EnumMap<>(proposal)); + if (session.getAttribute(CLIENT_PROPOSAL_MADE) != null) { + return; + } + String kexAlgorithms = proposal.get(KexProposalOption.SERVERKEYS); + if (GenericUtils.isEmpty(kexAlgorithms)) { + return; + } + List<String> algorithms = new ArrayList<>(); + // We're a client. We mustn't send the server extension, and we should send the client extension only once. + for (String algo : kexAlgorithms.split(",")) { //$NON-NLS-1$ + if (KexExtensions.CLIENT_KEX_EXTENSION.equalsIgnoreCase(algo) + || KexExtensions.SERVER_KEX_EXTENSION.equalsIgnoreCase(algo)) { + continue; + } + algorithms.add(algo); + } + // Tell the server that we want to receive SSH_MSG_EXT_INFO + algorithms.add(KexExtensions.CLIENT_KEX_EXTENSION); if (log.isDebugEnabled()) { - log.debug("handleKexInitProposal({})[initiator={}] proposal={}", session, initiator, proposal); + log.debug("handleKexInitProposal({}): proposing HostKeyAlgorithms {}", //$NON-NLS-1$ + session, algorithms); } - return; + proposal.put(KexProposalOption.SERVERKEYS, String.join(",", algorithms)); //$NON-NLS-1$ + session.setAttribute(CLIENT_PROPOSAL_MADE, Boolean.TRUE); } @Override public boolean handleKexExtensionRequest( Session session, int index, int count, String name, byte[] data) throws IOException { - if (!ServerSignatureAlgorithms.NAME.equalsIgnoreCase(name)) { - return true; // process next extension (if available) + if (ServerSignatureAlgorithms.NAME.equals(name)) { + handleServerSignatureAlgorithms(session, ServerSignatureAlgorithms.INSTANCE.parseExtension(data)); } - - Collection<String> sigAlgos = ServerSignatureAlgorithms.INSTANCE.parseExtension(data); - updateAvailableSignatureFactories(session, sigAlgos); - return false; // don't care about any more extensions (for now) - } - - public List<NamedFactory<Signature>> updateAvailableSignatureFactories( - Session session, Collection<String> extraAlgos) - throws IOException { - List<NamedFactory<Signature>> available = session.getSignatureFactories(); - List<NamedFactory<Signature>> updated = resolveUpdatedSignatureFactories(session, available, extraAlgos); - if (!UnaryEquator.isSameReference(available, updated)) { - if (log.isDebugEnabled()) { - log.debug("updateAvailableSignatureFactories({}) available={}, updated={}", - session, available, updated); - } - session.setSignatureFactories(updated); - } - - return updated; + return true; } /** - * Checks if the extra signature algorithms are already included in the available ones, and adds the extra ones (if - * supported). + * Perform updates after a server-sig-algs extension has been received. * - * @param session The {@link Session} for which the resolution occurs - * @param available The available signature factories - * @param extraAlgos The extra requested signatures - ignored if {@code null}/empty - * @return The resolved signature factories - same as input if nothing added - * @throws IOException If failed to resolve the factories + * @param session + * the message was received for + * @param serverAlgorithms + * signature algorithm names announced by the server */ - public List<NamedFactory<Signature>> resolveUpdatedSignatureFactories( - Session session, List<NamedFactory<Signature>> available, Collection<String> extraAlgos) - throws IOException { - boolean debugEnabled = log.isDebugEnabled(); - List<NamedFactory<Signature>> toAdd = resolveRequestedSignatureFactories(session, extraAlgos); - if (GenericUtils.isEmpty(toAdd)) { - if (debugEnabled) { - log.debug("resolveUpdatedSignatureFactories({}) Nothing to add to {} out of {}", - session, NamedResource.getNames(available), extraAlgos); - } - return available; - } - - for (int index = 0; index < toAdd.size(); index++) { - NamedFactory<Signature> f = toAdd.get(index); - String name = f.getName(); - NamedFactory<Signature> a = available.stream() - .filter(s -> Objects.equals(name, s.getName())) - .findFirst() - .orElse(null); - if (a == null) { - continue; - } - - if (debugEnabled) { - log.debug("resolveUpdatedSignatureFactories({}) skip {} - already available", session, name); - } - - toAdd.remove(index); - index--; // compensate for loop auto-increment - } - - return updateAvailableSignatureFactories(session, available, toAdd); - } - - public List<NamedFactory<Signature>> updateAvailableSignatureFactories( - Session session, List<NamedFactory<Signature>> available, Collection<? extends NamedFactory<Signature>> toAdd) - throws IOException { - boolean debugEnabled = log.isDebugEnabled(); - if (GenericUtils.isEmpty(toAdd)) { - if (debugEnabled) { - log.debug("updateAvailableSignatureFactories({}) nothing to add to {}", - session, NamedResource.getNames(available)); - } - return available; - } - - List<NamedFactory<Signature>> updated = new ArrayList<>(available.size() + toAdd.size()); - updated.addAll(available); - - for (NamedFactory<Signature> f : toAdd) { - int index = resolvePreferredSignaturePosition(session, updated, f); - if (debugEnabled) { - log.debug("updateAvailableSignatureFactories({}) add {} at position={}", session, f, index); - } - if ((index < 0) || (index >= updated.size())) { - updated.add(f); - } else { - updated.add(index, f); - } - } - - return updated; - } - - public int resolvePreferredSignaturePosition( - Session session, List<? extends NamedFactory<Signature>> factories, NamedFactory<Signature> factory) - throws IOException { - return SignatureFactory.resolvePreferredSignaturePosition(factories, factory); - } - - public List<NamedFactory<Signature>> resolveRequestedSignatureFactories( - Session session, Collection<String> extraAlgos) - throws IOException { - if (GenericUtils.isEmpty(extraAlgos)) { - return Collections.emptyList(); + protected void handleServerSignatureAlgorithms(Session session, Collection<String> serverAlgorithms) { + if (log.isDebugEnabled()) { + log.debug("handleServerSignatureAlgorithms({}): {}", session, //$NON-NLS-1$ + serverAlgorithms); } - - List<NamedFactory<Signature>> toAdd = Collections.emptyList(); - boolean debugEnabled = log.isDebugEnabled(); - for (String algo : extraAlgos) { - NamedFactory<Signature> factory = resolveRequestedSignatureFactory(session, algo); - if (factory == null) { - if (debugEnabled) { - log.debug("resolveRequestedSignatureFactories({}) skip {} - no factory found", session, algo); - } - continue; + // Client determines order; server says what it supports. Re-order such that supported ones are + // at the front, in client order, followed by unsupported ones, also in client order. + if (serverAlgorithms != null && !serverAlgorithms.isEmpty()) { + List<NamedFactory<Signature>> clientAlgorithms = session.getSignatureFactories(); + if (log.isDebugEnabled()) { + log.debug("handleServerSignatureAlgorithms({}): PubkeyAcceptedAlgorithms before: {}", //$NON-NLS-1$ + session, clientAlgorithms); } - - if ((factory instanceof OptionalFeature) && (!((OptionalFeature) factory).isSupported())) { - if (debugEnabled) { - log.debug("resolveRequestedSignatureFactories({}) skip {} - not supported", session, algo); + List<NamedFactory<Signature>> unknown = new ArrayList<>(); + Set<String> known = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + known.addAll(serverAlgorithms); + for (Iterator<NamedFactory<Signature>> i = clientAlgorithms.iterator(); i.hasNext(); ) { + NamedFactory<Signature> algo = i.next(); + if (!known.contains(algo.getName())) { + unknown.add(algo); + i.remove(); } - continue; } - - if (toAdd.isEmpty()) { - toAdd = new ArrayList<>(extraAlgos.size()); + // Re-add the unknown ones at the end. Per RFC 8308, some servers may not announce _all_ their + // supported algorithms, and a client may use unknown algorithms. + clientAlgorithms.addAll(unknown); + if (log.isDebugEnabled()) { + log.debug("handleServerSignatureAlgorithms({}): PubkeyAcceptedAlgorithms after: {}", //$NON-NLS-1$ + session, clientAlgorithms); } - toAdd.add(factory); + session.setSignatureFactories(clientAlgorithms); } - - return toAdd; - } - - public NamedFactory<Signature> resolveRequestedSignatureFactory(Session session, String name) throws IOException { - return BuiltinSignatures.fromFactoryName(name); } }