From 6e1d534f378e5d3080d2122cab0f19eaf1808ba8 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Thu, 9 Jul 2026 16:47:34 -0700 Subject: [PATCH] fix: send SSH_MSG_DISCONNECT on KEX failure Tell the peer why the handshake died instead of dropping the connection silently. RFC 4253 7.1: when no algorithm satisfying the conditions can be found, the connection fails and both sides MUST disconnect. wolfSSH already terminated the connection; the notification was missing. - Send a best-effort SSH_MSG_DISCONNECT with reason WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED at the DoKexInit() exit when no KEX, host key, cipher, or MAC could be matched. - Don't answer a KEXINIT that failed to negotiate. DoPacket() ran its rekey response on ssh->isKeying and ssh->connectState alone, so a post-auth rekey with a mismatch could emit a KEXDH packet immediately after the disconnect just sent. - Share the four-code predicate between the two sites as IsKexMatchError(). The DoPacket() gate cannot be written ret == WS_SUCCESS: a healthy client-side rekey returns WS_REKEYING out of DoKexInit(), so that form suppresses every rekey response. Issue: F-608 --- src/internal.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index 0bc01e114..35b5e73df 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5077,6 +5077,14 @@ static void SetPeerAlgoIds(HandshakeInfo* hs, } +/* The KEXINIT negotiation failures DoKexInit() answers with a disconnect. */ +static int IsKexMatchError(int ret) +{ + return ret == WS_MATCH_KEX_ALGO_E || ret == WS_MATCH_KEY_ALGO_E || + ret == WS_MATCH_ENC_ALGO_E || ret == WS_MATCH_MAC_ALGO_E; +} + + static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) { int ret = WS_SUCCESS; @@ -5473,6 +5481,10 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) ret = ssh->error; } } + /* RFC 4253 7.1: no common algorithm means both sides disconnect. */ + if (IsKexMatchError(ret)) { + (void)SendDisconnect(ssh, WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED); + } WLOG(WS_LOG_DEBUG, "Leaving DoKexInit(), ret = %d", ret); return ret; } @@ -11674,7 +11686,10 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed) case MSGID_KEXINIT: WLOG(WS_LOG_DEBUG, "Decoding MSGID_KEXINIT"); ret = DoKexInit(ssh, buf + idx, payloadSz, &payloadIdx); - if (ssh->isKeying && + /* Don't answer a KEXINIT that failed to negotiate; DoKexInit() + * has already disconnected. A healthy rekey lands here as + * WS_REKEYING, so this cannot test for WS_SUCCESS. */ + if (!IsKexMatchError(ret) && ssh->isKeying && ssh->connectState == CONNECT_SERVER_CHANNEL_REQUEST_DONE) { if (ssh->handshake->kexId == ID_DH_GEX_SHA256) { #if !defined(WOLFSSH_NO_DH) && !defined(WOLFSSH_NO_DH_GEX_SHA256)