fix: send SSH_MSG_DISCONNECT on KEX failure#1091
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Ensures the library sends a best-effort SSH_MSG_DISCONNECT when KEX negotiation fails due to no mutually supported algorithms, aligning behavior with RFC 4253 guidance and existing disconnect-on-error patterns in the codebase.
Changes:
- Adds a best-effort
(void)SendDisconnect(...)call inDoKexInit()when KEX/hostkey/cipher/MAC matching fails. - Uses
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILEDas the disconnect reason for these negotiation failures.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
aidangarske
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 2 total — 2 posted, 0 skipped
Posted findings
- [High] Disconnect can be followed by another KEX packet during rekey failure —
src/internal.c:5335-5338 - [Medium] Missing regression coverage for the new disconnect side effect —
src/internal.c:5335-5338
Review generated by Skoll.
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
64f19fe to
6e1d534
Compare
Bug
DoKexInit()insrc/internal.csetsretto one of the negotiation-failure codesWS_MATCH_KEX_ALGO_E/WS_MATCH_KEY_ALGO_E/WS_MATCH_ENC_ALGO_E/WS_MATCH_MAC_ALGO_Ewhen no common KEX / host-key / cipher / MAC algorithm can be negotiated, then returns. That code propagates unchanged throughDoPacket()andDoReceive(), which setssh->errorand returnWS_FATAL_ERROR. NoSSH_MSG_DISCONNECTis ever sent on these paths, so the peer is dropped without notification.RFC 4253 s7.1 covers exactly this case: when no algorithm satisfying all the conditions can be found, "the connection fails, and both sides MUST disconnect." wolfSSH already terminated the connection, so the MUST was met -- what was missing is the notification telling the peer why. The library already sends one best-effort with
(void)SendDisconnect(...)for service-request/accept errors (internal.c:7703, 7735); the KEX negotiation paths were inconsistent with that pattern.Fix
Two changes, sharing one predicate:
At the single
DoKexInit()exit point, send a best-effortSSH_MSG_DISCONNECTwith reasonWOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED(3) whenretis any of the four match-failure codes. Uses the same(void)SendDisconnectbest-effort form as the existing call sites.retis not overwritten for match errors -- theSendKexInit/ssh->errorpropagation block is gated onret == WS_SUCCESS.In
DoPacket(), don't answer a KEXINIT that failed to negotiate. TheMSGID_KEXINITcase ran its rekey response (SendKexDhInit()/SendKexDhGexRequest()) based only onssh->isKeyingandssh->connectState, never consultingret, so a post-auth rekey with a mismatch could emit a KEXDH packet immediately after the disconnect just sent.Both sites test the same predicate through a shared helper:
The second gate cannot be written as
ret == WS_SUCCESS. A healthy client-side rekey returnsWS_REKEYING(-1035) out ofDoKexInit()-- a status, not an error, carried by the trailingif (ssh->error != 0) ret = ssh->error;that propagates a want-write fromSendKexInit(). Gating onWS_SUCCESSsuppresses every rekey response, not just failing ones, and fails the SCP rekey test attests/api.c:3030.Verification
Built
--enable-allagainst a full wolfSSL install.make check: 10 pass, 1 skip, 0 fail.Behavior confirmed on the wire with a raw probe that completes the version exchange, sends a KEXINIT with one bogus name-list, and reads to EOF. Against
examples/echoserver, for each of the four negotiation fields (kex, host key, cipher, MAC): master replies with its own KEXINIT and then closes silently; with this change it replies KEXINIT followed bySSH_MSG_DISCONNECTwith reason 3. An OpenSSH client will not surface this -- it computes the algorithm intersection itself and aborts before reading the server's reply, which is why a raw probe was needed.Regression coverage for the new disconnect side effect is deferred;
tests/regress.casserts theWS_MATCH_*return codes but does not yet capture the outbound packet.Reported by static analysis (Fenrir finding 608). Severity Low -- the connection already terminated; this adds the reason code.