@@ -750,6 +750,9 @@ static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz)
750750#define REGRESS_MUTATE_TERRAPIN_C2S 9
751751/* The splice ahead of the server's KEXINIT, before the allow list arms. */
752752#define REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT 10
753+ /* The splice ahead of the client's KEXDH_INIT, while the server waits for
754+ * the message that opens the negotiated KEX. */
755+ #define REGRESS_MUTATE_TERRAPIN_PRE_KEXDH 11
753756
754757/* 4 (len) + 1 (padLen) + 1 (msgId) + 4 (empty string payload) + 6 (pad) */
755758#define REGRESS_INJECT_PACKET_SZ 16U
@@ -1368,8 +1371,14 @@ static void TerrapinInject(DuplexEndpoint* endpoint,
13681371 byte injectPkt[REGRESS_INJECT_MAX_SZ];
13691372 word32 injectPktSz;
13701373 word32 anchorOffset;
1371- byte anchorMsgId = (mutator->mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT)
1372- ? MSGID_KEXINIT : MSGID_NEWKEYS;
1374+ byte anchorMsgId = MSGID_NEWKEYS;
1375+
1376+ if (mutator->mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT) {
1377+ anchorMsgId = MSGID_KEXINIT;
1378+ }
1379+ else if (mutator->mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXDH) {
1380+ anchorMsgId = MSGID_KEXDH_INIT;
1381+ }
13731382
13741383 if (mutator->injectedPackets > 0) {
13751384 return;
@@ -1401,15 +1410,17 @@ static int IsTerrapinMode(byte mode)
14011410{
14021411 return mode == REGRESS_MUTATE_TERRAPIN ||
14031412 mode == REGRESS_MUTATE_TERRAPIN_C2S ||
1404- mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT;
1413+ mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT ||
1414+ mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXDH;
14051415}
14061416
1407- /* E_TRUNC, E_EMPTY and TERRAPIN_C2S rewrite the client's messages; the
1408- * rest rewrite the server's. */
1417+ /* E_TRUNC, E_EMPTY, TERRAPIN_C2S and TERRAPIN_PRE_KEXDH rewrite the
1418+ * client's messages; the rest rewrite the server's. */
14091419static int MutatorTargetsEndpoint(byte mode, byte isServer)
14101420{
14111421 if (mode == REGRESS_MUTATE_E_TRUNC || mode == REGRESS_MUTATE_E_EMPTY ||
1412- mode == REGRESS_MUTATE_TERRAPIN_C2S) {
1422+ mode == REGRESS_MUTATE_TERRAPIN_C2S ||
1423+ mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXDH) {
14131424 return !isServer;
14141425 }
14151426 return isServer != 0;
@@ -2725,6 +2736,69 @@ static void TestPreKexInitInjectionAcceptedWithoutStrictKex(void)
27252736}
27262737#endif /* !WOLFSSH_NO_AES_GCM */
27272738
2739+ /* Between the client's KEXINIT and its KEXDH_INIT the server accepts only
2740+ * the message that opens the negotiated KEX, so one from another method or
2741+ * an early NEWKEYS ends the connection (draft-ietf-sshm-strict-kex
2742+ * section 3.2). */
2743+ static void AssertOutOfTurnKexMsgRejected(byte injectMsgId)
2744+ {
2745+ KexReplyHarness harness;
2746+ KexReplyRunResult result;
2747+
2748+ InitStrictKexHarnessMode(&harness, injectMsgId, 1, 1,
2749+ REGRESS_MUTATE_TERRAPIN_PRE_KEXDH);
2750+ RunKexReplyHandshake(&harness, &result);
2751+
2752+ AssertIntEQ(harness.mutator.injectedPackets, 1);
2753+ AssertIntEQ(harness.server->useStrictKex, 1);
2754+
2755+ AssertFalse(result.serverSuccess);
2756+ AssertIntEQ(result.serverErr, WS_MSGID_NOT_ALLOWED_E);
2757+ AssertIntEQ(harness.server->initialKexDone, 0);
2758+ AssertIntEQ(harness.server->disconnected, 1);
2759+
2760+ AssertIntEQ(harness.serverIo.sawDisconnect, 1);
2761+ AssertIntEQ(harness.serverIo.disconnectReason,
2762+ WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
2763+
2764+ FreeKexReplyHarness(&harness);
2765+ }
2766+
2767+ static void TestOutOfTurnKexMsgRejectedWithStrictKex(void)
2768+ {
2769+ static const byte outOfTurn[] = {
2770+ MSGID_NEWKEYS, MSGID_KEXDH_REPLY, MSGID_KEXDH_GEX_INIT,
2771+ MSGID_KEXDH_GEX_REQUEST
2772+ };
2773+ word32 i;
2774+
2775+ for (i = 0; i < (word32)(sizeof(outOfTurn) / sizeof(outOfTurn[0])); i++) {
2776+ AssertOutOfTurnKexMsgRejected(outOfTurn[i]);
2777+ }
2778+ }
2779+
2780+ /* A NEWKEYS that installs no keys (the injected one carries a payload) is
2781+ * counted like any other packet: no counter reset, and the initial KEX is
2782+ * not over. The server has counted KEXINIT, KEXDH_INIT and this one. */
2783+ static void TestStrictKexFailedNewKeysNotCounted(void)
2784+ {
2785+ KexReplyHarness harness;
2786+ KexReplyRunResult result;
2787+
2788+ InitStrictKexHarnessMode(&harness, MSGID_NEWKEYS, 1, 1,
2789+ REGRESS_MUTATE_TERRAPIN_C2S);
2790+ RunKexReplyHandshake(&harness, &result);
2791+
2792+ AssertIntEQ(harness.mutator.injectedPackets, 1);
2793+ AssertIntEQ(harness.server->useStrictKex, 1);
2794+
2795+ AssertFalse(result.serverSuccess);
2796+ AssertIntEQ(harness.server->initialKexDone, 0);
2797+ AssertIntEQ(harness.server->peerSeq, 3);
2798+
2799+ FreeKexReplyHarness(&harness);
2800+ }
2801+
27282802/* A strict KEX peer ends the connection on an IGNORE during the initial
27292803 * KEX, so wolfSSH_SendIgnore() refuses until that KEX is done. */
27302804static void TestStrictKexHoldsSendIgnore(void)
@@ -15604,7 +15678,7 @@ static void RunFirstPacketFollowsSkipCase(FirstPacketFollowsSkipFn fn,
1560415678/* After skipping a wrong-guess packet, the real first packet of the negotiated
1560515679 * KEX must still pass the message gate even when it sits across the GEX
1560615680 * boundary from the guess. Drives skip-then-real through IsMessageAllowed and
15607- * checks the skip did not pin expectMsgId to the discarded message's ID . */
15681+ * checks the skip set expectMsgId from the negotiated KEX, not the guess . */
1560815682static void RunFirstPacketFollowsCrossBoundaryCase(FirstPacketFollowsSkipFn fn,
1560915683 const char* label, byte realMsg)
1561015684{
@@ -15624,15 +15698,17 @@ static void RunFirstPacketFollowsCrossBoundaryCase(FirstPacketFollowsSkipFn fn,
1562415698 ssh->isKeying |= WOLFSSH_PEER_IS_KEYING;
1562515699 ssh->handshake->ignoreNextKexMsg = 1;
1562615700 ssh->handshake->expectMsgId = MSGID_NONE;
15701+ ssh->handshake->kexId = (realMsg == MSGID_KEXDH_GEX_REQUEST) ?
15702+ ID_DH_GEX_SHA256 : ID_DH_GROUP14_SHA256;
1562715703
1562815704 WMEMSET(payload, 0xAB, sizeof(payload));
1562915705
1563015706 ret = fn(ssh, payload, sizeof(payload), &idx);
1563115707 if (ret != WS_SUCCESS)
1563215708 Fail(("%s returns WS_SUCCESS when skipping", label), ("%d", ret));
1563315709
15634- /* Gate must not be pinned to the discarded guess's ID . */
15635- AssertIntEQ(ssh->handshake->expectMsgId, MSGID_NONE );
15710+ /* Gate is set by the negotiated KEX, not the discarded guess. */
15711+ AssertIntEQ(ssh->handshake->expectMsgId, realMsg );
1563615712 /* The negotiated first-KEX packet must be accepted. */
1563715713 AssertIntEQ(wolfSSH_TestIsMessageAllowed(ssh, realMsg, 0), 1);
1563815714
@@ -18493,6 +18569,8 @@ int main(int argc, char** argv)
1849318569 TestTerrapinInjectionRejectedByServer();
1849418570 TestStrictKexTakesInjectedDisconnect();
1849518571 TestPreKexInitInjectionRejectedWithStrictKex();
18572+ TestOutOfTurnKexMsgRejectedWithStrictKex();
18573+ TestStrictKexFailedNewKeysNotCounted();
1849618574 TestStrictKexHoldsSendIgnore();
1849718575 #ifndef WOLFSSH_NO_AES_GCM
1849818576 TestPreKexInitInjectionAcceptedWithoutStrictKex();
0 commit comments