@@ -740,6 +740,8 @@ static word32 LoadFileBuffer(const char* path, byte* buf, word32 bufSz)
740740/* The same splice aimed the other way: the packet goes in ahead of the
741741 * client's NEWKEYS, so the server is the one asked to accept it. */
742742#define REGRESS_MUTATE_TERRAPIN_C2S 9
743+ /* The splice ahead of the server's KEXINIT, before the allow list arms. */
744+ #define REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT 10
743745
744746/* 4 (len) + 1 (padLen) + 1 (msgId) + 4 (empty string payload) + 6 (pad) */
745747#define REGRESS_INJECT_PACKET_SZ 16U
@@ -1351,49 +1353,51 @@ static int FindPlainPacketOffset(const byte* packet, word32 packetSz,
13511353 * cipher state, so a deletion breaks decryption outright. The injection
13521354 * alone is what these tests exercise, and refusing it is what the
13531355 * mitigation has to do. */
1354- static void TerrapinInjectBeforeNewKeys (DuplexEndpoint* endpoint,
1356+ static void TerrapinInject (DuplexEndpoint* endpoint,
13551357 const byte** output, word32* outputSz)
13561358{
13571359 KexReplyMutator* mutator = endpoint->mutator;
13581360 byte injectPkt[REGRESS_INJECT_MAX_SZ];
13591361 word32 injectPktSz;
1360- word32 newKeysOffset;
1362+ word32 anchorOffset;
1363+ byte anchorMsgId = (mutator->mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT)
1364+ ? MSGID_KEXINIT : MSGID_NEWKEYS;
13611365
13621366 if (mutator->injectedPackets > 0) {
13631367 return;
13641368 }
1365- if (!FindPlainPacketOffset(*output, *outputSz, MSGID_NEWKEYS ,
1366- &newKeysOffset )) {
1369+ if (!FindPlainPacketOffset(*output, *outputSz, anchorMsgId ,
1370+ &anchorOffset )) {
13671371 return;
13681372 }
13691373
13701374 injectPktSz = BuildInjectedPacket(injectPkt, (word32)sizeof(injectPkt),
13711375 mutator->injectMsgId);
13721376
1373- /* Everything ahead of NEWKEYS passes through untouched , then the forged
1374- * packet; the caller forwards the NEWKEYS tail . */
1375- if (newKeysOffset > 0) {
1377+ /* Pass through what precedes the anchor , then the forged packet; the
1378+ * caller forwards the rest . */
1379+ if (anchorOffset > 0) {
13761380 AssertIntEQ(QueueAppend(&endpoint->peer->inbound, *output,
1377- newKeysOffset ), WS_SUCCESS);
1381+ anchorOffset ), WS_SUCCESS);
13781382 }
13791383 AssertIntEQ(QueueAppend(&endpoint->peer->inbound, injectPkt,
13801384 injectPktSz), WS_SUCCESS);
13811385
13821386 mutator->injectedPackets++;
1383- *output += newKeysOffset ;
1384- *outputSz -= newKeysOffset ;
1387+ *output += anchorOffset ;
1388+ *outputSz -= anchorOffset ;
13851389}
13861390
1387- /* Both splice a packet into the initial KEX, they only differ in which
1388- * side's NEWKEYS they go in ahead of. */
1391+ /* Each splices a packet into the initial KEX ahead of a different one. */
13891392static int IsTerrapinMode(byte mode)
13901393{
13911394 return mode == REGRESS_MUTATE_TERRAPIN ||
1392- mode == REGRESS_MUTATE_TERRAPIN_C2S;
1395+ mode == REGRESS_MUTATE_TERRAPIN_C2S ||
1396+ mode == REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT;
13931397}
13941398
1395- /* SIG_*, F_TRUNC, TERRAPIN and the GEX_* modes rewrite the server's
1396- * messages; E_TRUNC, E_EMPTY and TERRAPIN_C2S the client 's. */
1399+ /* E_TRUNC, E_EMPTY and TERRAPIN_C2S rewrite the client's messages; the
1400+ * rest rewrite the server 's. */
13971401static int MutatorTargetsEndpoint(byte mode, byte isServer)
13981402{
13991403 if (mode == REGRESS_MUTATE_E_TRUNC || mode == REGRESS_MUTATE_E_EMPTY ||
@@ -1489,7 +1493,7 @@ static int DuplexSend(WOLFSSH* ssh, void* buf, word32 sz, void* ctx)
14891493 !(outputSz >= REGRESS_SSH_PROTO_PREFIX_SZ &&
14901494 WMEMCMP(output, REGRESS_SSH_PROTO_PREFIX,
14911495 REGRESS_SSH_PROTO_PREFIX_SZ) == 0)) {
1492- TerrapinInjectBeforeNewKeys (endpoint, &output, &outputSz);
1496+ TerrapinInject (endpoint, &output, &outputSz);
14931497 }
14941498
14951499 if (endpoint->mutator != NULL &&
@@ -2702,6 +2706,69 @@ static void TestStrictKexTakesInjectedDisconnect(void)
27022706 FreeKexReplyHarness(&harness);
27032707}
27042708
2709+ /* A packet ahead of the server's KEXINIT passes the unarmed allow list,
2710+ * so the KEXINIT arrives at a nonzero sequence number and the client
2711+ * disconnects. */
2712+ static void AssertPreKexInitInjectionRejected(byte injectMsgId)
2713+ {
2714+ KexReplyHarness harness;
2715+ KexReplyRunResult result;
2716+
2717+ InitStrictKexHarnessMode(&harness, injectMsgId, 1, 1,
2718+ REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT);
2719+ RunKexReplyHandshake(&harness, &result);
2720+
2721+ AssertIntEQ(harness.mutator.injectedPackets, 1);
2722+ AssertIntEQ(harness.mutator.parseError, 0);
2723+ AssertIntEQ(harness.client->strictKexEnabled, 1);
2724+
2725+ AssertFalse(result.clientSuccess);
2726+ AssertIntEQ(result.clientErr, WS_MSGID_NOT_ALLOWED_E);
2727+ AssertIntEQ(harness.client->initialKexDone, 0);
2728+ AssertIntEQ(harness.client->disconnected, 1);
2729+
2730+ /* Still plaintext, so the reason is readable. */
2731+ AssertIntEQ(harness.clientIo.sawDisconnect, 1);
2732+ AssertIntEQ(harness.clientIo.disconnectReason,
2733+ WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
2734+
2735+ FreeKexReplyHarness(&harness);
2736+ }
2737+
2738+ static void TestPreKexInitInjectionRejectedWithStrictKex(void)
2739+ {
2740+ /* EXT_INFO would replace the client's server-sig-algs list. DEBUG is
2741+ * omitted: it gets decoded here, and the payload is too short. */
2742+ static const byte injectable[] = {
2743+ MSGID_IGNORE, MSGID_UNIMPLEMENTED, MSGID_EXT_INFO,
2744+ REGRESS_UNASSIGNED_TRANS_MSGID
2745+ };
2746+ word32 i;
2747+
2748+ for (i = 0; i < (word32)(sizeof(injectable) / sizeof(injectable[0])); i++) {
2749+ AssertPreKexInitInjectionRejected(injectable[i]);
2750+ }
2751+ }
2752+
2753+ /* Without strict KEX the same splice is accepted. */
2754+ static void TestPreKexInitInjectionAcceptedWithoutStrictKex(void)
2755+ {
2756+ KexReplyHarness harness;
2757+ KexReplyRunResult result;
2758+
2759+ InitStrictKexHarnessMode(&harness, MSGID_IGNORE, 0, 0,
2760+ REGRESS_MUTATE_TERRAPIN_PRE_KEXINIT);
2761+ RunKexReplyHandshake(&harness, &result);
2762+
2763+ AssertIntEQ(harness.mutator.injectedPackets, 1);
2764+ AssertIntEQ(harness.mutator.parseError, 0);
2765+ AssertIntEQ(harness.client->strictKexEnabled, 0);
2766+ AssertTrue(result.clientSuccess);
2767+ AssertTrue(result.serverSuccess);
2768+
2769+ FreeKexReplyHarness(&harness);
2770+ }
2771+
27052772/* What a worker call is allowed to come back with while a rekey runs: an
27062773 * empty queue, a packet handed to the application, or the rekey itself
27072774 * still in progress. Anything else is a real failure. */
@@ -17449,6 +17516,8 @@ int main(int argc, char** argv)
1744917516 TestTerrapinInjectionRejectedWithStrictKex();
1745017517 TestTerrapinInjectionRejectedByServer();
1745117518 TestStrictKexTakesInjectedDisconnect();
17519+ TestPreKexInitInjectionRejectedWithStrictKex();
17520+ TestPreKexInitInjectionAcceptedWithoutStrictKex();
1745217521#endif
1745317522
1745417523#ifdef WOLFSSH_SFTP
0 commit comments