Skip to content

Commit 1ddd486

Browse files
ejohnstownphilljj
authored andcommitted
internal: expect the opening KEX message
After the client's KEXINIT the server left expectMsgId unset until the client's first KEX message arrived, so any transport ID got through in that window: a message from another KEX method, or an early NEWKEYS. Strict KEX allows only the messages the negotiated method expects (draft-ietf-sshm-strict-kex section 3.2). - DoKexInit() sets expectMsgId to KEXDH_INIT, or KEXDH_GEX_REQUEST for DH-GEX; after a wrong first_kex_packet_follows guess it is set once the guessed packet is skipped - DoPacket() resets peerSeq and ends the initial KEX only on a NEWKEYS that installed keys; one that fails is counted like any other packet - regress: out-of-turn KEX messages ahead of the client's KEXDH_INIT end the connection under strict KEX, and a failed NEWKEYS is counted - regress: the first_kex_packet_follows cross-boundary case expects the negotiated KEX's message after the skip
1 parent 3b2dd60 commit 1ddd486

2 files changed

Lines changed: 125 additions & 15 deletions

File tree

‎src/internal.c‎

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6574,6 +6574,19 @@ static int IsKexMatchError(int ret)
65746574
#define WOLFSSH_KEXINIT_ID_LIST_MAX 32
65756575
#endif
65766576

6577+
#ifndef NO_WOLFSSH_SERVER
6578+
/* Server: after KEXINIT, accept only the message that opens the negotiated
6579+
* KEX (draft-ietf-sshm-strict-kex section 3.2). */
6580+
static void ExpectClientKexMsg(WOLFSSH* ssh)
6581+
{
6582+
ssh->handshake->expectMsgId =
6583+
(ssh->handshake->kexId == ID_DH_GEX_SHA256) ?
6584+
MSGID_KEXDH_GEX_REQUEST : MSGID_KEXDH_INIT;
6585+
WLOG_EXPECT_MSGID(ssh->handshake->expectMsgId);
6586+
}
6587+
#endif
6588+
6589+
65776590
static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
65786591
{
65796592
int ret = WS_SUCCESS;
@@ -7005,8 +7018,14 @@ static int DoKexInit(WOLFSSH* ssh, byte* buf, word32 len, word32* idx)
70057018

70067019
if (ret == WS_SUCCESS) {
70077020
*idx = begin;
7008-
if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)
7021+
if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) {
70097022
ssh->clientState = CLIENT_KEXINIT_DONE;
7023+
#ifndef NO_WOLFSSH_SERVER
7024+
/* A wrong guess sets it once the guess is skipped. */
7025+
if (!ssh->handshake->ignoreNextKexMsg)
7026+
ExpectClientKexMsg(ssh);
7027+
#endif
7028+
}
70107029
else
70117030
ssh->serverState = SERVER_KEXINIT_DONE;
70127031

@@ -7221,9 +7240,9 @@ static const word32 dhPrimeGroup16Sz = (word32)sizeof(dhPrimeGroup16);
72217240
#endif
72227241

72237242
/* RFC 4253 sec. 7.1: discard the KEX packet a peer sent after a wrong
7224-
* first_packet_follows guess. Leave expectMsgId at MSGID_NONE, the real
7225-
* follow-up's ID comes from the negotiated KEX, not the guess, and may cross
7226-
* the GEX boundary. Returns 1 if consumed, else 0. Caller validates handshake. */
7243+
* first_packet_follows guess. expectMsgId stays at MSGID_NONE until then,
7244+
* since the guessed packet's ID may cross the GEX boundary. Returns 1 if
7245+
* consumed, else 0. Caller validates handshake. */
72277246
static int SkipGuessedKexMsg(WOLFSSH* ssh, const char* what,
72287247
word32 len, word32* idx)
72297248
{
@@ -7234,6 +7253,10 @@ static int SkipGuessedKexMsg(WOLFSSH* ssh, const char* what,
72347253
what);
72357254
ssh->handshake->ignoreNextKexMsg = 0;
72367255
*idx += len;
7256+
#ifndef NO_WOLFSSH_SERVER
7257+
if (ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER)
7258+
ExpectClientKexMsg(ssh);
7259+
#endif
72377260
return 1;
72387261
}
72397262

@@ -13956,6 +13979,7 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1395613979
byte msg;
1395713980
word32 payloadIdx = 0;
1395813981
int msgAllowed;
13982+
byte newKeysDone = 0;
1395913983
int ret = WS_SUCCESS;
1396013984

1396113985
WLOG(WS_LOG_DEBUG, "DoPacket sequence number: %d", ssh->peerSeq);
@@ -14088,9 +14112,16 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1408814112
break;
1408914113

1409014114
case MSGID_NEWKEYS:
14115+
{
14116+
/* DoNewKeys() frees the handshake once the keys are in, even
14117+
* when the window flush after that wants a write. */
14118+
byte hadHandshake = (ssh->handshake != NULL);
14119+
1409114120
WLOG(WS_LOG_DEBUG, "Decoding MSGID_NEWKEYS");
1409214121
ret = DoNewKeys(ssh, buf + idx, payloadSz, &payloadIdx);
14122+
newKeysDone = hadHandshake && ssh->handshake == NULL;
1409314123
break;
14124+
}
1409414125

1409514126
case MSGID_KEXDH_INIT:
1409614127
WLOG(WS_LOG_DEBUG, "Decoding MSGID_KEXDH_INIT");
@@ -14257,10 +14288,11 @@ static int DoPacket(WOLFSSH* ssh, byte* bufferConsumed)
1425714288
idx = len;
1425814289
}
1425914290
ssh->inputBuffer.idx = idx;
14260-
if (msg == MSGID_NEWKEYS) {
14291+
if (newKeysDone) {
1426114292
/* Strict KEX (Terrapin mitigation): once negotiated, every
1426214293
* SSH_MSG_NEWKEYS resets the incoming sequence number so the
14263-
* next inbound packet starts at zero under the new keys. */
14294+
* next inbound packet starts at zero under the new keys. A
14295+
* NEWKEYS that installed no keys is counted like any other. */
1426414296
if (ssh->useStrictKex) {
1426514297
ssh->peerSeq = 0;
1426614298
}

‎tests/regress.c‎

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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. */
14091419
static 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. */
27302804
static 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. */
1560815682
static 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

Comments
 (0)