@@ -585,7 +585,7 @@ const char* GetErrorString(int err)
585585 return "not a regular file";
586586
587587 case WS_MSGID_NOT_ALLOWED_E:
588- return "message not allowed before user authentication ";
588+ return "message ID not allowed at this point in the connection ";
589589
590590 case WS_ED25519_E:
591591 return "Ed25519 buffer error";
@@ -671,8 +671,9 @@ static INLINE int HighwaterCheck(WOLFSSH* ssh, byte side)
671671
672672 /* RFC 4344 Sec 3.1: rekey before the 32-bit SSH sequence number wraps
673673 * to prevent MAC/nonce reuse. Counter is per-key (resets on rekey),
674- * not the absolute ssh->seq (which does not reset); default 2^31
675- * keeps each epoch comfortably under the 2^32 wrap. */
674+ * not the absolute ssh->seq (which resets only at a strict KEX
675+ * NEWKEYS); default 2^31 keeps each epoch comfortably under the
676+ * 2^32 wrap. */
676677 if (!ssh->msgHighwaterFlag && ssh->msgHighwaterMark &&
677678 (ssh->txMsgCount >= ssh->msgHighwaterMark ||
678679 ssh->rxMsgCount >= ssh->msgHighwaterMark)) {
@@ -725,6 +726,27 @@ static HandshakeInfo* HandshakeInfoNew(void* heap)
725726}
726727
727728
729+ /* The strict KEX setting this handshake runs on. Frozen the first time it
730+ * is read -- at our KEXINIT or at the peer's, whichever comes first -- so
731+ * that a caller toggling the setting mid-handshake cannot leave one side
732+ * advertising the marker and the other refusing to enforce it. A later
733+ * toggle takes effect on the next handshake, which allocates its own
734+ * HandshakeInfo. */
735+ static byte HandshakeStrictKex(WOLFSSH* ssh)
736+ {
737+ if (ssh->handshake == NULL) {
738+ return ssh->sendStrictKex;
739+ }
740+
741+ if (!ssh->handshake->strictKexSet) {
742+ ssh->handshake->strictKex = ssh->sendStrictKex ? 1 : 0;
743+ ssh->handshake->strictKexSet = 1;
744+ }
745+
746+ return ssh->handshake->strictKex;
747+ }
748+
749+
728750static void HandshakeInfoFree(HandshakeInfo* hs, void* heap)
729751{
730752 WOLFSSH_UNUSED(heap);
@@ -1019,6 +1041,27 @@ INLINE static int IsMessageAllowedClient(WOLFSSH *ssh, byte msg)
10191041 * send some of those ids the other way. */
10201042INLINE static int IsMessageAllowed(WOLFSSH *ssh, byte msg, byte state)
10211043{
1044+ /* Strict KEX (Terrapin mitigation). Nothing in the initial KEX is
1045+ * authenticated, so any packet spliced into it shifts the receiver's
1046+ * sequence number, and that shift is the attack. Take an allow list
1047+ * rather than name the messages to refuse: until the peer's NEWKEYS
1048+ * lands, the only things it can legitimately send are DISCONNECT and
1049+ * the key exchange itself. IGNORE, DEBUG, UNIMPLEMENTED, EXT_INFO and
1050+ * the unassigned transport IDs are all otherwise accepted here, and
1051+ * every one of them counts against peerSeq. DISCONNECT stays allowed
1052+ * so a peer can still tear the connection down. */
1053+ if (state == WS_MSG_RECV && ssh->strictKexEnabled &&
1054+ !ssh->initialKexDone) {
1055+ if (msg != MSGID_DISCONNECT && msg != MSGID_KEXINIT &&
1056+ msg != MSGID_NEWKEYS && !MSGIDLIMIT_TRANS_KEX(msg)) {
1057+ WLOG(WS_LOG_DEBUG,
1058+ "Message ID %u not allowed during the initial strict KEX",
1059+ msg);
1060+ ssh->error = WS_MSGID_NOT_ALLOWED_E;
1061+ return 0;
1062+ }
1063+ }
1064+
10221065#ifndef NO_WOLFSSH_SERVER
10231066 if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {
10241067 return IsMessageAllowedServer(ssh, msg);
@@ -1399,6 +1442,7 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap)
13991442 ctx->windowSz = DEFAULT_WINDOW_SZ;
14001443 ctx->maxPacketSz = DEFAULT_MAX_PACKET_SZ;
14011444 ctx->maxAuthAttempts = DEFAULT_MAX_AUTH_ATTEMPTS;
1445+ ctx->sendStrictKex = 1; /* default-enabled, callers can opt out */
14021446 ctx->sshProtoIdStr = sshProtoIdStr;
14031447 ctx->sshProtoIdStrSz = (word32)(sizeof(sshProtoIdStr) - 1);
14041448 ctx->algoListKex = cannedKexAlgoNames;
@@ -1795,6 +1839,7 @@ WOLFSSH* SshInit(WOLFSSH* ssh, WOLFSSH_CTX* ctx)
17951839 ssh->acceptState = ACCEPT_BEGIN;
17961840 ssh->clientState = CLIENT_BEGIN;
17971841 ssh->isKeying = 0; /* initial state of not keying yet */
1842+ ssh->sendStrictKex = ctx->sendStrictKex;
17981843 ssh->authId = ID_USERAUTH_PUBLICKEY;
17991844 ssh->supportedAuth[0] = ID_USERAUTH_PUBLICKEY;
18001845 ssh->supportedAuth[1] = ID_USERAUTH_PASSWORD;
@@ -3787,8 +3832,16 @@ static const NameIdPair NameIdMap[] = {
37873832 { ID_CURVE25519_SHA256, TYPE_KEX, "curve25519-sha256" },
37883833 { ID_CURVE25519_SHA256_LIBSSH, TYPE_KEX, "curve25519-sha256@libssh.org" },
37893834#endif
3790- { ID_EXTINFO_S, TYPE_OTHER, "ext-info-s" },
3791- { ID_EXTINFO_C, TYPE_OTHER, "ext-info-c" },
3835+ { ID_EXT_INFO_S, TYPE_OTHER, "ext-info-s" },
3836+ { ID_EXT_INFO_C, TYPE_OTHER, "ext-info-c" },
3837+ /* Strict KEX marker. draft-miller-sshm-strict-kex defines the
3838+ * unprefixed names for eventual IETF standardization; deployed
3839+ * OpenSSH only sends the -v00@openssh.com ones. Advertise and accept
3840+ * both spellings. */
3841+ { ID_EXT_STRICT_KEX_S, TYPE_OTHER, "kex-strict-s" },
3842+ { ID_EXT_STRICT_KEX_C, TYPE_OTHER, "kex-strict-c" },
3843+ { ID_EXT_PRE_STRICT_KEX_S, TYPE_OTHER, "kex-strict-s-v00@openssh.com" },
3844+ { ID_EXT_PRE_STRICT_KEX_C, TYPE_OTHER, "kex-strict-c-v00@openssh.com" },
37923845
37933846 /* Public Key IDs */
37943847#ifndef WOLFSSH_NO_RSA
@@ -6651,12 +6704,45 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
66516704
66526705 /* Match the peer accepts extInfo. */
66536706 algoId = (side == WOLFSSH_ENDPOINT_SERVER)
6654- ? ID_EXTINFO_C : ID_EXTINFO_S ;
6707+ ? ID_EXT_INFO_C : ID_EXT_INFO_S ;
66556708 extInfo = MatchIdLists(side, list, listSz, &algoId, 1);
66566709 ssh->sendExtInfo = extInfo == algoId;
66576710 }
66586711 }
66596712
6713+ /* Strict KEX marker (Terrapin mitigation). Only valid in the initial
6714+ * KEXINIT (sessionIdSz == 0); if the peer offers it during a rekey,
6715+ * ignore the marker per draft-miller-sshm-strict-kex. */
6716+ if (ret == WS_SUCCESS) {
6717+ if (ssh->sessionIdSz == 0) {
6718+ /* OpenSSH only ever sends the -v00@openssh.com name, so both
6719+ * spellings have to be accepted for the marker to negotiate
6720+ * against a real peer. */
6721+ byte expectedStrict[2];
6722+ byte matched;
6723+
6724+ if (side == WOLFSSH_ENDPOINT_SERVER) {
6725+ expectedStrict[0] = ID_EXT_STRICT_KEX_C;
6726+ expectedStrict[1] = ID_EXT_PRE_STRICT_KEX_C;
6727+ }
6728+ else {
6729+ expectedStrict[0] = ID_EXT_STRICT_KEX_S;
6730+ expectedStrict[1] = ID_EXT_PRE_STRICT_KEX_S;
6731+ }
6732+ matched = MatchIdLists(side, list, listSz, expectedStrict, 2);
6733+ ssh->peerStrictKex = (matched != ID_UNKNOWN);
6734+ ssh->strictKexEnabled =
6735+ ssh->peerStrictKex && HandshakeStrictKex(ssh);
6736+ if (ssh->strictKexEnabled) {
6737+ WLOG(WS_LOG_DEBUG, "DKI: strict KEX negotiated");
6738+ }
6739+ }
6740+ else {
6741+ WLOG(WS_LOG_DEBUG,
6742+ "DKI: rekey, ignoring any peer strict KEX marker");
6743+ }
6744+ }
6745+
66606746 /* Server Host Key Algorithms */
66616747 if (ret == WS_SUCCESS) {
66626748 WLOG(WS_LOG_DEBUG, "DKI: Server Host Key Algorithms");
@@ -13917,6 +14003,20 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1391714003
1391814004 msgAllowed = IsMessageAllowed(ssh, msg, WS_MSG_RECV);
1391914005
14006+ if (!msgAllowed && ssh->strictKexEnabled && !ssh->initialKexDone) {
14007+ /* Strict KEX calls for terminating the connection, not just
14008+ * dropping the packet, so tell the peer why on the way out. Every
14009+ * refused id ends it here, the unassigned ones an UNIMPLEMENTED
14010+ * would otherwise answer included. The error is relatched because
14011+ * the send path can overwrite it. */
14012+ if (!ssh->disconnected) {
14013+ (void)SendDisconnect(ssh,
14014+ WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
14015+ ssh->error = WS_MSGID_NOT_ALLOWED_E;
14016+ }
14017+ return WS_MSGID_NOT_ALLOWED_E;
14018+ }
14019+
1392014020 if (!msgAllowed && (MsgIdKnown(msg) || MSGIDLIMIT_POST_USERAUTH(msg))) {
1392114021 /* RFC 4252 section 6: disconnect on a known id at the wrong time,
1392214022 * and on any id of 80 or higher, which IsMessageAllowed() refuses
@@ -14168,7 +14268,23 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1416814268 idx = len;
1416914269 }
1417014270 ssh->inputBuffer.idx = idx;
14171- ssh->peerSeq++;
14271+ if (msg == MSGID_NEWKEYS) {
14272+ /* Strict KEX (Terrapin mitigation): once negotiated, every
14273+ * SSH_MSG_NEWKEYS resets the incoming sequence number so the
14274+ * next inbound packet starts at zero under the new keys. */
14275+ if (ssh->strictKexEnabled) {
14276+ ssh->peerSeq = 0;
14277+ }
14278+ else {
14279+ ssh->peerSeq++;
14280+ }
14281+ /* The peer's initial KEX is over, so IGNORE, DEBUG, and
14282+ * UNIMPLEMENTED are legal from it again. */
14283+ ssh->initialKexDone = 1;
14284+ }
14285+ else {
14286+ ssh->peerSeq++;
14287+ }
1417214288 ssh->rxMsgCount++;
1417314289 *bufferConsumed = 1;
1417414290
@@ -15260,12 +15376,36 @@ int SendKexInit(WOLFSSH* ssh)
1526015376 }
1526115377
1526215378 if (ret == WS_SUCCESS) {
15379+ /* The strict-kex marker is only advertised during the initial KEX
15380+ * (RFC draft-miller-sshm-strict-kex), distinguished here by an
15381+ * empty session id. The sendStrictKex flag lets callers opt out
15382+ * at runtime; the handshake freezes it so this and the enforcement
15383+ * decision in DoKexInit() cannot read different values. */
15384+ int includeStrictKex =
15385+ (ssh->sessionIdSz == 0) && HandshakeStrictKex(ssh);
15386+
1526315387 if (ssh->ctx->side == WOLFSSH_ENDPOINT_CLIENT) {
15264- kexAlgoNamesPlus = ",ext-info-c";
15388+ if (includeStrictKex) {
15389+ kexAlgoNamesPlus =
15390+ ",ext-info-c"
15391+ ",kex-strict-c-v00@openssh.com"
15392+ ",kex-strict-c";
15393+ }
15394+ else {
15395+ kexAlgoNamesPlus = ",ext-info-c";
15396+ }
1526515397 kexAlgoNamesPlusSz = (word32)WSTRLEN(kexAlgoNamesPlus);
1526615398 }
1526715399 else {
15268- kexAlgoNamesPlus = ",ext-info-s";
15400+ if (includeStrictKex) {
15401+ kexAlgoNamesPlus =
15402+ ",ext-info-s"
15403+ ",kex-strict-s-v00@openssh.com"
15404+ ",kex-strict-s";
15405+ }
15406+ else {
15407+ kexAlgoNamesPlus = ",ext-info-s";
15408+ }
1526915409 kexAlgoNamesPlusSz = (word32)WSTRLEN(kexAlgoNamesPlus);
1527015410 }
1527115411
@@ -18717,6 +18857,16 @@ int SendNewKeys(WOLFSSH* ssh)
1871718857 ssh->outputBuffer.length = idx;
1871818858
1871918859 ret = BundlePacket(ssh);
18860+
18861+ /* Strict KEX (Terrapin mitigation): once negotiated, every
18862+ * SSH_MSG_NEWKEYS resets the outgoing sequence number so the next
18863+ * outbound packet starts at zero under the new keys. This has to
18864+ * happen here, before SendPendingChannelWindowAdjust() below can
18865+ * bundle a packet, or that packet goes out at the old sequence
18866+ * number while the peer MACs it at zero. */
18867+ if (ret == WS_SUCCESS && ssh->strictKexEnabled) {
18868+ ssh->seq = 0;
18869+ }
1872018870 }
1872118871
1872218872 if (ret == WS_SUCCESS) {
0 commit comments