Skip to content

Commit 297dd45

Browse files
committed
Fix TLS ext bounds checking
1 parent 53e3521 commit 297dd45

5 files changed

Lines changed: 362 additions & 20 deletions

File tree

src/tls.c

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6493,7 +6493,7 @@ static int TLSX_SessionTicket_Parse(WOLFSSL* ssl, const byte* input,
64936493
return ret;
64946494
}
64956495

6496-
WOLFSSL_LOCAL SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
6496+
WOLFSSL_TEST_VIS SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
64976497
byte* data, word16 size, void* heap)
64986498
{
64996499
SessionTicket* ticket = (SessionTicket*)XMALLOC(sizeof(SessionTicket),
@@ -6514,7 +6514,7 @@ WOLFSSL_LOCAL SessionTicket* TLSX_SessionTicket_Create(word32 lifetime,
65146514

65156515
return ticket;
65166516
}
6517-
WOLFSSL_LOCAL void TLSX_SessionTicket_Free(SessionTicket* ticket, void* heap)
6517+
WOLFSSL_TEST_VIS void TLSX_SessionTicket_Free(SessionTicket* ticket, void* heap)
65186518
{
65196519
if (ticket) {
65206520
XFREE(ticket->data, heap, DYNAMIC_TYPE_TLSX);
@@ -14868,9 +14868,27 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1486814868
{
1486914869
int ret = 0;
1487014870
TLSX* extension;
14871-
word16 length = 0;
14871+
/* Use a word32 accumulator so that an extension whose contribution
14872+
* pushes the running total past 0xFFFF is detected rather than
14873+
* silently wrapped (the TLS extensions block length prefix on the
14874+
* wire is a 2-byte field). Callees that take a word16* accumulator
14875+
* are invoked via a per-iteration shim (`cbShim`) and their delta
14876+
* is added back into the word32 total.
14877+
*
14878+
* MAINTAINER NOTE: do NOT pass &length to any *_GET_SIZE function
14879+
* that expects a `word16*` out-parameter -- that would be a type
14880+
* mismatch (UB) and would silently bypass the overflow detection
14881+
* below. When adding a new extension case, either:
14882+
* - use `length += FOO_GET_SIZE(...)` when the helper returns a
14883+
* word16 by value, or
14884+
* - use the cbShim pattern: `cbShim = 0; ret = FOO_GET_SIZE(...,
14885+
* &cbShim); length += cbShim;`
14886+
*/
14887+
word32 length = 0;
14888+
word16 cbShim = 0;
1487214889
byte isRequest = (msgType == client_hello ||
1487314890
msgType == certificate_request);
14891+
(void)cbShim;
1487414892

1487514893
while ((extension = list)) {
1487614894
list = extension->next;
@@ -14954,23 +14972,31 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1495414972
#endif
1495514973
#if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY)
1495614974
case TLSX_ENCRYPT_THEN_MAC:
14957-
ret = ETM_GET_SIZE(msgType, &length);
14975+
cbShim = 0;
14976+
ret = ETM_GET_SIZE(msgType, &cbShim);
14977+
length += cbShim;
1495814978
break;
1495914979
#endif /* HAVE_ENCRYPT_THEN_MAC */
1496014980

1496114981
#if defined(WOLFSSL_TLS13) || !defined(WOLFSSL_NO_TLS12) || !defined(NO_OLD_TLS)
1496214982
#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)
1496314983
case TLSX_PRE_SHARED_KEY:
14984+
cbShim = 0;
1496414985
ret = PSK_GET_SIZE((PreSharedKey*)extension->data, msgType,
14965-
&length);
14986+
&cbShim);
14987+
length += cbShim;
1496614988
break;
1496714989
#ifdef WOLFSSL_TLS13
1496814990
case TLSX_PSK_KEY_EXCHANGE_MODES:
14969-
ret = PKM_GET_SIZE((byte)extension->val, msgType, &length);
14991+
cbShim = 0;
14992+
ret = PKM_GET_SIZE((byte)extension->val, msgType, &cbShim);
14993+
length += cbShim;
1497014994
break;
1497114995
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
1497214996
case TLSX_CERT_WITH_EXTERN_PSK:
14973-
ret = PSK_WITH_CERT_GET_SIZE(msgType, &length);
14997+
cbShim = 0;
14998+
ret = PSK_WITH_CERT_GET_SIZE(msgType, &cbShim);
14999+
length += cbShim;
1497415000
break;
1497515001
#endif
1497615002
#endif
@@ -14982,22 +15008,30 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1498215008

1498315009
#ifdef WOLFSSL_TLS13
1498415010
case TLSX_SUPPORTED_VERSIONS:
14985-
ret = SV_GET_SIZE(extension->data, msgType, &length);
15011+
cbShim = 0;
15012+
ret = SV_GET_SIZE(extension->data, msgType, &cbShim);
15013+
length += cbShim;
1498615014
break;
1498715015

1498815016
case TLSX_COOKIE:
14989-
ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &length);
15017+
cbShim = 0;
15018+
ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &cbShim);
15019+
length += cbShim;
1499015020
break;
1499115021

1499215022
#ifdef WOLFSSL_EARLY_DATA
1499315023
case TLSX_EARLY_DATA:
14994-
ret = EDI_GET_SIZE(msgType, &length);
15024+
cbShim = 0;
15025+
ret = EDI_GET_SIZE(msgType, &cbShim);
15026+
length += cbShim;
1499515027
break;
1499615028
#endif
1499715029

1499815030
#ifdef WOLFSSL_POST_HANDSHAKE_AUTH
1499915031
case TLSX_POST_HANDSHAKE_AUTH:
15000-
ret = PHA_GET_SIZE(msgType, &length);
15032+
cbShim = 0;
15033+
ret = PHA_GET_SIZE(msgType, &cbShim);
15034+
length += cbShim;
1500115035
break;
1500215036
#endif
1500315037

@@ -15050,12 +15084,26 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType,
1505015084
break;
1505115085
}
1505215086

15087+
/* Early exit: stop accumulating as soon as the running total
15088+
* cannot possibly fit the 2-byte wire length. Check *before*
15089+
* marking the extension as processed so the semaphore is not
15090+
* left in an inconsistent state on the error path. */
15091+
if (length > WOLFSSL_MAX_16BIT) {
15092+
WOLFSSL_MSG("TLSX_GetSize extension length exceeds word16");
15093+
return BUFFER_E;
15094+
}
15095+
1505315096
/* marks the extension as processed so ctx level */
1505415097
/* extensions don't overlap with ssl level ones. */
1505515098
TURN_ON(semaphore, TLSX_ToSemaphore((word16)extension->type));
1505615099
}
1505715100

15058-
*pLength += length;
15101+
if ((word32)*pLength + length > WOLFSSL_MAX_16BIT) {
15102+
WOLFSSL_MSG("TLSX_GetSize total extensions length exceeds word16");
15103+
return BUFFER_E;
15104+
}
15105+
15106+
*pLength += (word16)length;
1505915107

1506015108
return ret;
1506115109
}
@@ -15068,6 +15116,7 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1506815116
TLSX* extension;
1506915117
word16 offset = 0;
1507015118
word16 length_offset = 0;
15119+
word32 prevOffset;
1507115120
byte isRequest = (msgType == client_hello ||
1507215121
msgType == certificate_request);
1507315122

@@ -15082,6 +15131,10 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1508215131
if (!IS_OFF(semaphore, TLSX_ToSemaphore((word16)extension->type)))
1508315132
continue; /* skip! */
1508415133

15134+
/* Snapshot offset to detect word16 wrap within this iteration;
15135+
* see matching comment in TLSX_GetSize. */
15136+
prevOffset = offset;
15137+
1508515138
/* writes extension type. */
1508615139
c16toa((word16)extension->type, output + offset);
1508715140
offset += HELLO_EXT_TYPE_SZ + OPAQUE16_LEN;
@@ -15315,9 +15368,24 @@ static int TLSX_Write(TLSX* list, byte* output, byte* semaphore,
1531515368
/* if we encountered an error propagate it */
1531615369
if (ret != 0)
1531715370
break;
15371+
15372+
if (offset <= prevOffset) {
15373+
WOLFSSL_MSG("TLSX_Write extension length exceeds word16");
15374+
return BUFFER_E;
15375+
}
1531815376
}
1531915377

15320-
*pOffset += offset;
15378+
/* Only validate and commit the aggregate offset when the loop
15379+
* completed without error; on the error path, leave *pOffset
15380+
* unchanged and return the original failure reason so callers
15381+
* see the real error instead of a masking BUFFER_E. */
15382+
if (ret == 0) {
15383+
if ((word32)*pOffset + (word32)offset > WOLFSSL_MAX_16BIT) {
15384+
WOLFSSL_MSG("TLSX_Write total extensions length exceeds word16");
15385+
return BUFFER_E;
15386+
}
15387+
*pOffset += offset;
15388+
}
1532115389

1532215390
return ret;
1532315391
}
@@ -16421,6 +16489,13 @@ int TLSX_GetRequestSize(WOLFSSL* ssl, byte msgType, word32* pLength)
1642116489
}
1642216490
#endif
1642316491

16492+
/* The TLS extensions block length prefix is a 2-byte field, so any
16493+
* accumulated total above 0xFFFF must be rejected rather than silently
16494+
* truncating and producing a short, malformed handshake message. */
16495+
if (length > (word16)(WOLFSSL_MAX_16BIT - OPAQUE16_LEN)) {
16496+
WOLFSSL_MSG("TLSX_GetRequestSize extensions exceed word16");
16497+
return BUFFER_E;
16498+
}
1642416499
if (length)
1642516500
length += OPAQUE16_LEN; /* for total length storage. */
1642616501

@@ -16624,6 +16699,12 @@ int TLSX_WriteRequest(WOLFSSL* ssl, byte* output, byte msgType, word32* pOffset)
1662416699
#endif
1662516700
#endif
1662616701

16702+
/* Wrap detection for the TLSX_Write calls above is handled inside
16703+
* TLSX_Write itself: any iteration that would push the local word16
16704+
* offset past 0xFFFF returns BUFFER_E so we never reach here with a
16705+
* truncated value. The TLS extensions block length prefix on the
16706+
* wire is a 2-byte field, matching this invariant. */
16707+
1662716708
if (offset > OPAQUE16_LEN || msgType != client_hello)
1662816709
c16toa(offset - OPAQUE16_LEN, output); /* extensions length */
1662916710

tests/api.c

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10578,6 +10578,144 @@ static int test_tls_ext_duplicate(void)
1057810578
return EXPECT_RESULT();
1057910579
}
1058010580

10581+
/* Regression test: TLSX extension size accumulation must not silently wrap
10582+
* the internal word16 accumulator. Prior to the fix, a single extension
10583+
* whose size (plus the 4-byte header) pushes the running total past 0xFFFF
10584+
* caused TLSX_GetSize / TLSX_Write to return a truncated length, which
10585+
* in turn led to undersized buffer writes. The on-wire extensions block
10586+
* length is a 2-byte field per RFC 8446 Section 4.2, so the correct
10587+
* behavior is to return BUFFER_E rather than wrap. */
10588+
static int test_tls_ext_word16_overflow(void)
10589+
{
10590+
EXPECT_DECLS;
10591+
#if defined(HAVE_SESSION_TICKET) && !defined(NO_WOLFSSL_CLIENT) && \
10592+
!defined(NO_TLS)
10593+
WOLFSSL_CTX* ctx = NULL;
10594+
WOLFSSL* ssl = NULL;
10595+
SessionTicket* ticket = NULL;
10596+
byte* big = NULL;
10597+
/* Size chosen so that 4 (ext header) + size > 0xFFFF. */
10598+
const word16 bigSz = 0xFFFE;
10599+
word32 length = 0;
10600+
10601+
big = (byte*)XMALLOC(bigSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10602+
ExpectNotNull(big);
10603+
if (big != NULL)
10604+
XMEMSET(big, 0xA5, bigSz);
10605+
10606+
ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method()));
10607+
ExpectNotNull(ssl = wolfSSL_new(ctx));
10608+
10609+
/* Build an oversized SessionTicket extension directly on the ssl
10610+
* extension list. Going via the public API is not enough here because
10611+
* wolfSSL_set_SessionTicket clamps to word16 without creating the
10612+
* TLSX entry; the TLSX path is what exercises the accumulator. */
10613+
if (EXPECT_SUCCESS()) {
10614+
ticket = TLSX_SessionTicket_Create(0, big, bigSz, ssl->heap);
10615+
ExpectNotNull(ticket);
10616+
}
10617+
if (EXPECT_SUCCESS()) {
10618+
ExpectIntEQ(TLSX_UseSessionTicket(&ssl->extensions, ticket, ssl->heap),
10619+
WOLFSSL_SUCCESS);
10620+
/* TLSX_UseSessionTicket takes ownership on success. */
10621+
ticket = NULL;
10622+
}
10623+
10624+
/* TLSX_GetRequestSize must refuse to encode: 4-byte ext header +
10625+
* 0xFFFE payload + 2-byte block length prefix = 0x10004, which does
10626+
* not fit in a word16 wire length. Expect BUFFER_E, not a silently
10627+
* wrapped small value. */
10628+
if (EXPECT_SUCCESS()) {
10629+
int ret = TLSX_GetRequestSize(ssl, client_hello, &length);
10630+
ExpectIntEQ(ret, BUFFER_E);
10631+
}
10632+
10633+
if (ticket != NULL)
10634+
TLSX_SessionTicket_Free(ticket, ssl->heap);
10635+
wolfSSL_free(ssl);
10636+
wolfSSL_CTX_free(ctx);
10637+
XFREE(big, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10638+
ssl = NULL;
10639+
ctx = NULL;
10640+
big = NULL;
10641+
10642+
/* Boundary case: construct a SessionTicket extension sized so that the
10643+
* total extensions length in TLSX_GetRequestSize is exactly
10644+
* WOLFSSL_MAX_16BIT - OPAQUE16_LEN (0xFFFD) *before* the OPAQUE16_LEN
10645+
* block-prefix adjustment, which must succeed. This pins the `>`
10646+
* comparison in the overflow check -- mutating it to `>=` would
10647+
* incorrectly reject this valid case and fail this test. */
10648+
{
10649+
WOLFSSL_CTX* ctx2 = NULL;
10650+
WOLFSSL* ssl2 = NULL;
10651+
SessionTicket* ticket2 = NULL;
10652+
byte* buf = NULL;
10653+
word32 baseLen = 0;
10654+
word32 baseInternal = 0;
10655+
word32 tickSz = 0;
10656+
/* TLSX_GetRequestSize rejects when internal sum > 0xFFFD. */
10657+
const word32 target = (word32)WOLFSSL_MAX_16BIT - (word32)OPAQUE16_LEN;
10658+
/* Session ticket extension contributes: type (2) + len (2) + size. */
10659+
const word32 extHdr = (word32)HELLO_EXT_TYPE_SZ
10660+
+ (word32)OPAQUE16_LEN;
10661+
int ret;
10662+
10663+
ExpectNotNull(ctx2 = wolfSSL_CTX_new(wolfSSLv23_client_method()));
10664+
ExpectNotNull(ssl2 = wolfSSL_new(ctx2));
10665+
10666+
/* Measure baseline length with no session ticket extension. The
10667+
* returned value already includes the 2-byte block-length prefix
10668+
* when nonzero; strip it to get the raw internal sum. */
10669+
if (EXPECT_SUCCESS()) {
10670+
baseLen = 0;
10671+
ret = TLSX_GetRequestSize(ssl2, client_hello, &baseLen);
10672+
ExpectIntEQ(ret, 0);
10673+
baseInternal = (baseLen > 0)
10674+
? baseLen - (word32)OPAQUE16_LEN : 0;
10675+
}
10676+
10677+
/* Target: baseInternal + extHdr + tickSz == 0xFFFD. */
10678+
if (EXPECT_SUCCESS() && baseInternal + extHdr < target) {
10679+
tickSz = target - baseInternal - extHdr;
10680+
}
10681+
10682+
if (EXPECT_SUCCESS() && tickSz > 0 && tickSz <= WOLFSSL_MAX_16BIT) {
10683+
buf = (byte*)XMALLOC(tickSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10684+
ExpectNotNull(buf);
10685+
if (buf != NULL)
10686+
XMEMSET(buf, 0x5A, tickSz);
10687+
}
10688+
10689+
if (EXPECT_SUCCESS() && buf != NULL) {
10690+
ticket2 = TLSX_SessionTicket_Create(0, buf, (word16)tickSz,
10691+
ssl2->heap);
10692+
ExpectNotNull(ticket2);
10693+
}
10694+
if (EXPECT_SUCCESS() && ticket2 != NULL) {
10695+
ExpectIntEQ(TLSX_UseSessionTicket(&ssl2->extensions, ticket2,
10696+
ssl2->heap), WOLFSSL_SUCCESS);
10697+
ticket2 = NULL;
10698+
}
10699+
10700+
/* Exact boundary: internal sum == 0xFFFD must succeed, and the
10701+
* final returned length is 0xFFFD + OPAQUE16_LEN == 0xFFFF. */
10702+
if (EXPECT_SUCCESS()) {
10703+
word32 lenBoundary = 0;
10704+
ret = TLSX_GetRequestSize(ssl2, client_hello, &lenBoundary);
10705+
ExpectIntEQ(ret, 0);
10706+
ExpectIntEQ(lenBoundary, (word32)WOLFSSL_MAX_16BIT);
10707+
}
10708+
10709+
if (ticket2 != NULL)
10710+
TLSX_SessionTicket_Free(ticket2, ssl2->heap);
10711+
wolfSSL_free(ssl2);
10712+
wolfSSL_CTX_free(ctx2);
10713+
XFREE(buf, NULL, DYNAMIC_TYPE_TMP_BUFFER);
10714+
}
10715+
#endif
10716+
return EXPECT_RESULT();
10717+
}
10718+
1058110719

1058210720
/* Test TLS connection abort when legacy version field indicates TLS 1.3 or
1058310721
* higher. Based on test_tls_ext_duplicate() but with legacy version modified
@@ -36664,6 +36802,7 @@ TEST_CASE testCases[] = {
3666436802
TEST_DECL(test_wolfSSL_SCR_Reconnect),
3666536803
TEST_DECL(test_wolfSSL_SCR_check_enabled),
3666636804
TEST_DECL(test_tls_ext_duplicate),
36805+
TEST_DECL(test_tls_ext_word16_overflow),
3666736806
TEST_DECL(test_tls_bad_legacy_version),
3666836807
#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH)
3666936808
#if defined(HAVE_IO_TESTS_DEPENDENCIES)

wolfcrypt/src/aes.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16852,6 +16852,11 @@ int wc_AesEaxDecryptAuth(const byte* key, word32 keySz, byte* out,
1685216852
return BAD_FUNC_ARG;
1685316853
}
1685416854

16855+
if (authTagSz < WOLFSSL_MIN_AUTH_TAG_SZ
16856+
|| authTagSz > WC_AES_BLOCK_SIZE) {
16857+
return BAD_FUNC_ARG;
16858+
}
16859+
1685516860
#if defined(WOLFSSL_SMALL_STACK)
1685616861
if ((eax = (AesEax *)XMALLOC(sizeof(AesEax),
1685716862
NULL,
@@ -17201,8 +17206,8 @@ int wc_AesEaxDecryptFinal(AesEax* eax,
1720117206
byte authTag[WC_AES_BLOCK_SIZE];
1720217207
#endif
1720317208

17204-
if (eax == NULL || authIn == NULL || authInSz == 0 ||
17205-
authInSz > WC_AES_BLOCK_SIZE) {
17209+
if (eax == NULL || authIn == NULL || authInSz > WC_AES_BLOCK_SIZE
17210+
|| authInSz < WOLFSSL_MIN_AUTH_TAG_SZ) {
1720617211
return BAD_FUNC_ARG;
1720717212
}
1720817213

0 commit comments

Comments
 (0)