Skip to content

Commit 4869a3c

Browse files
committed
internal: require KEXINIT first under strict KEX
The strict KEX allow list is armed only once the peer's KEXINIT has negotiated it, so a packet spliced in ahead of that KEXINIT got through. DoKexInit() now ends the connection with KEY_EXCHANGE_FAILED when strict KEX is on and the KEXINIT is not the peer's first packet. - regress: a harness mode that splices a packet in ahead of the server's KEXINIT - regress: IGNORE, UNIMPLEMENTED, EXT_INFO and an unassigned id there are refused with strict KEX; an IGNORE is accepted without it
1 parent 75ab378 commit 4869a3c

2 files changed

Lines changed: 96 additions & 16 deletions

File tree

‎src/internal.c‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6736,6 +6736,17 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
67366736
if (ssh->strictKexEnabled) {
67376737
WLOG(WS_LOG_DEBUG, "DKI: strict KEX negotiated");
67386738
}
6739+
6740+
/* Strict KEX requires KEXINIT to be the peer's first packet.
6741+
* The allow list is not armed until now. peerSeq is still
6742+
* this packet's number here. */
6743+
if (ssh->strictKexEnabled && ssh->peerSeq != 0) {
6744+
WLOG(WS_LOG_DEBUG,
6745+
"DKI: strict KEX, KEXINIT was not the first packet");
6746+
(void)SendDisconnect(ssh,
6747+
WOLFSSH_DISCONNECT_KEY_EXCHANGE_FAILED);
6748+
ret = WS_MSGID_NOT_ALLOWED_E;
6749+
}
67396750
}
67406751
else {
67416752
WLOG(WS_LOG_DEBUG,

‎tests/regress.c‎

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
13891392
static 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. */
13971401
static 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

Comments
 (0)