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);
}