From b641d5d4063c52dae379bfc5e941c312677e7c85 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Fri, 24 Apr 2026 14:45:27 -0700 Subject: [PATCH] Client: check first_packet_follows guess in DoKexDhReply When a server sends first_kex_packet_follows=TRUE with an incorrect KEX algorithm guess, the client now silently discards the server's speculative KEXDH_REPLY message by checking ignoreNextKexMsg at the top of DoKexDhReply, matching the existing server-side handling in DoKexDhInit. Add regression test covering the client-side skip path. Affected functions: DoKexDhReply. Issue: F-2863 --- src/internal.c | 16 ++++++++++++++++ tests/regress.c | 25 +++++++++++++++++-------- wolfssh/internal.h | 2 ++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/internal.c b/src/internal.c index a74f447e4..aa8073bf7 100644 --- a/src/internal.c +++ b/src/internal.c @@ -5823,6 +5823,17 @@ static int DoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) return ret; } + if (ret == WS_SUCCESS) { + if (ssh->handshake->ignoreNextKexMsg) { + /* skip this message. */ + WLOG(WS_LOG_DEBUG, "Skipping server's KEXDH_REPLY message due to " + "first_packet_follows guess mismatch."); + ssh->handshake->ignoreNextKexMsg = 0; + *idx += len; + return WS_SUCCESS; + } + } + if (ret == WS_SUCCESS && len < LENGTH_SZ*2 + *idx) { ret = WS_BUFFER_E; } @@ -17901,6 +17912,11 @@ int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) return DoKexDhInit(ssh, buf, len, idx); } +int wolfSSH_TestDoKexDhReply(WOLFSSH* ssh, byte* buf, word32 len, word32* idx) +{ + return DoKexDhReply(ssh, buf, len, idx); +} + int wolfSSH_TestChannelPutData(WOLFSSH_CHANNEL* channel, byte* data, word32 dataSz) { diff --git a/tests/regress.c b/tests/regress.c index 5f1bec181..7d1dbb2be 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -2072,9 +2072,9 @@ typedef int (*FirstPacketFollowsSkipFn)(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); /* With ignoreNextKexMsg set, the target Do* handler must consume the packet, - * clear the flag, and not advance clientState past CLIENT_KEXINIT_DONE. */ + * clear the flag, and not advance the peer's state past KEXINIT_DONE. */ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, - const char* label) + const char* label, byte endpointType, byte initState) { WOLFSSH_CTX* ctx; WOLFSSH* ssh; @@ -2082,7 +2082,7 @@ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, word32 idx = 0; int ret; - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + ctx = wolfSSH_CTX_new(endpointType, NULL); AssertNotNull(ctx); ssh = wolfSSH_new(ctx); @@ -2090,9 +2090,12 @@ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, AssertNotNull(ssh->handshake); ssh->handshake->ignoreNextKexMsg = 1; - ssh->clientState = CLIENT_KEXINIT_DONE; + if (endpointType == WOLFSSH_ENDPOINT_SERVER) + ssh->clientState = initState; + else + ssh->serverState = initState; - /* Garbage payload — must never be parsed when skipped. */ + /* Garbage payload that must never be parsed when skipped. */ WMEMSET(payload, 0xAB, sizeof(payload)); ret = fn(ssh, payload, sizeof(payload), &idx); @@ -2101,7 +2104,10 @@ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, } AssertIntEQ(idx, sizeof(payload)); AssertIntEQ(ssh->handshake->ignoreNextKexMsg, 0); - AssertIntEQ(ssh->clientState, CLIENT_KEXINIT_DONE); + if (endpointType == WOLFSSH_ENDPOINT_SERVER) + AssertIntEQ(ssh->clientState, initState); + else + AssertIntEQ(ssh->serverState, initState); wolfSSH_free(ssh); wolfSSH_CTX_free(ctx); @@ -2109,11 +2115,14 @@ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn, static void TestFirstPacketFollowsSkipped(void) { - RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhInit, "DoKexDhInit"); + RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhInit, + "DoKexDhInit", WOLFSSH_ENDPOINT_SERVER, CLIENT_KEXINIT_DONE); #ifndef WOLFSSH_NO_DH_GEX_SHA256 RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhGexRequest, - "DoKexDhGexRequest"); + "DoKexDhGexRequest", WOLFSSH_ENDPOINT_SERVER, CLIENT_KEXINIT_DONE); #endif + RunFirstPacketFollowsSkipCase(wolfSSH_TestDoKexDhReply, + "DoKexDhReply", WOLFSSH_ENDPOINT_CLIENT, SERVER_KEXINIT_DONE); } static void TestFirstPacketFollows(void) diff --git a/wolfssh/internal.h b/wolfssh/internal.h index 5616b5556..73bd81f6b 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -1337,6 +1337,8 @@ enum WS_MessageIdLimits { word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestDoKexDhInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx); + WOLFSSH_API int wolfSSH_TestDoKexDhReply(WOLFSSH* ssh, byte* buf, + word32 len, word32* idx); WOLFSSH_API int wolfSSH_TestChannelPutData(WOLFSSH_CHANNEL*, byte*, word32); #ifndef WOLFSSH_NO_DH_GEX_SHA256 WOLFSSH_API int wolfSSH_TestDoKexDhGexRequest(WOLFSSH* ssh, byte* buf,