protected Map negotiate()

in sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java [2176:2322]


    protected Map<KexProposalOption, String> negotiate() throws Exception {
        Map<KexProposalOption, String> c2sOptions = getClientKexProposals();
        Map<KexProposalOption, String> s2cOptions = getServerKexProposals();
        signalNegotiationStart(c2sOptions, s2cOptions);

        // Make modifiable. Strict KEX flags are to be heeded only in initial KEX, and to be ignored afterwards.
        c2sOptions = new EnumMap<>(c2sOptions);
        s2cOptions = new EnumMap<>(s2cOptions);
        boolean strictKexClient = removeValue(c2sOptions, KexProposalOption.ALGORITHMS,
                KexExtensions.STRICT_KEX_CLIENT_EXTENSION);
        boolean strictKexServer = removeValue(s2cOptions, KexProposalOption.ALGORITHMS,
                KexExtensions.STRICT_KEX_SERVER_EXTENSION);
        if (removeValue(c2sOptions, KexProposalOption.ALGORITHMS, KexExtensions.STRICT_KEX_SERVER_EXTENSION)
                && !initialKexDone) {
            log.warn("negotiate({}) client proposal contains server flag {}; will be ignored", this,
                    KexExtensions.STRICT_KEX_SERVER_EXTENSION);
        }
        if (removeValue(s2cOptions, KexProposalOption.ALGORITHMS, KexExtensions.STRICT_KEX_CLIENT_EXTENSION)
                && !initialKexDone) {
            log.warn("negotiate({}) server proposal contains client flag {}; will be ignored", this,
                    KexExtensions.STRICT_KEX_CLIENT_EXTENSION);
        }
        // Make unmodifiable again
        c2sOptions = Collections.unmodifiableMap(c2sOptions);
        s2cOptions = Collections.unmodifiableMap(s2cOptions);
        Map<KexProposalOption, String> guess = new EnumMap<>(KexProposalOption.class);
        Map<KexProposalOption, String> negotiatedGuess = Collections.unmodifiableMap(guess);
        try {
            boolean debugEnabled = log.isDebugEnabled();
            boolean traceEnabled = log.isTraceEnabled();
            if (!initialKexDone) {
                strictKex = strictKexClient && strictKexServer;
                if (debugEnabled) {
                    log.debug("negotiate({}) strict KEX={} client={} server={}", this, strictKex, strictKexClient,
                            strictKexServer);
                }
                if (strictKex && initialKexInitSequenceNumber != 1) {
                    throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
                            "Strict KEX negotiated but sequence number of first KEX_INIT received is not 1: "
                                                                                             + initialKexInitSequenceNumber);
                }
            }
            SessionDisconnectHandler discHandler = getSessionDisconnectHandler();
            KexExtensionHandler extHandler = getKexExtensionHandler();
            for (KexProposalOption paramType : KexProposalOption.VALUES) {
                String clientParamValue = c2sOptions.get(paramType);
                String serverParamValue = s2cOptions.get(paramType);
                String[] c = GenericUtils.split(clientParamValue, ',');
                String[] s = GenericUtils.split(serverParamValue, ',');
                if (paramType == KexProposalOption.C2SMAC && isAead(guess.get(KexProposalOption.C2SENC)) ||
                        paramType == KexProposalOption.S2CMAC && isAead(guess.get(KexProposalOption.S2CENC))) {
                    // No MAC needed, so no need to negotiate. Set a value all the same, otherwise
                    // SessionContext.isDataIntegrityTransport() would be complicated quite a bit.
                    guess.put(paramType, "aead");
                    continue;
                }
                /*
                 * According to https://tools.ietf.org/html/rfc8308#section-2.2:
                 *
                 * Implementations MAY disconnect if the counterpart sends an incorrect (KEX extension) indicator
                 *
                 * TODO - for now we do not enforce this
                 */
                for (String ci : c) {
                    for (String si : s) {
                        if (ci.equals(si)) {
                            guess.put(paramType, ci);
                            break;
                        }
                    }

                    String value = guess.get(paramType);
                    if (value != null) {
                        break;
                    }
                }

                // check if reached an agreement
                String value = guess.get(paramType);
                if (extHandler != null) {
                    extHandler.handleKexExtensionNegotiation(
                            this, paramType, value, c2sOptions, clientParamValue, s2cOptions, serverParamValue);
                }

                if (value != null) {
                    if (traceEnabled) {
                        log.trace("negotiate({})[{}] guess={} (client={} / server={})",
                                this, paramType.getDescription(), value, clientParamValue, serverParamValue);
                    }
                    continue;
                }

                try {
                    if ((discHandler != null)
                            && discHandler.handleKexDisconnectReason(
                                    this, c2sOptions, s2cOptions, negotiatedGuess, paramType)) {
                        if (debugEnabled) {
                            log.debug("negotiate({}) ignore missing value for KEX option={}", this, paramType);
                        }
                        continue;
                    }
                } catch (IOException | RuntimeException e) {
                    // If disconnect handler throws an exception continue with the disconnect
                    debug("negotiate({}) failed ({}) to invoke disconnect handler due to mismatched KEX option={}: {}",
                            this, e.getClass().getSimpleName(), paramType, e.getMessage(), e);
                }

                String message = "Unable to negotiate key exchange for " + paramType.getDescription()
                                 + " (client: " + clientParamValue + " / server: " + serverParamValue + ")";
                // OK if could not negotiate languages
                if (KexProposalOption.S2CLANG.equals(paramType) || KexProposalOption.C2SLANG.equals(paramType)) {
                    if (traceEnabled) {
                        log.trace("negotiate({}) {}", this, message);
                    }
                } else {
                    throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED, message);
                }
            }

            /*
             * According to https://tools.ietf.org/html/rfc8308#section-2.2:
             *
             * If "ext-info-c" or "ext-info-s" ends up being negotiated as a key exchange method, the parties MUST
             * disconnect.
             */
            String kexOption = guess.get(KexProposalOption.ALGORITHMS);
            if (KexExtensions.IS_KEX_EXTENSION_SIGNAL.test(kexOption)) {
                if ((discHandler != null)
                        && discHandler.handleKexDisconnectReason(
                                this, c2sOptions, s2cOptions, negotiatedGuess, KexProposalOption.ALGORITHMS)) {
                    if (debugEnabled) {
                        log.debug("negotiate({}) ignore violating {} KEX option={}", this, KexProposalOption.ALGORITHMS,
                                kexOption);
                    }
                } else {
                    throw new SshException(SshConstants.SSH2_DISCONNECT_KEY_EXCHANGE_FAILED,
                            "Illegal KEX option negotiated: " + kexOption);
                }
            }
        } catch (IOException | RuntimeException | Error e) {
            signalNegotiationEnd(c2sOptions, s2cOptions, negotiatedGuess, e);
            throw e;
        }

        signalNegotiationEnd(c2sOptions, s2cOptions, negotiatedGuess, null);
        return setNegotiationResult(guess);
    }