Skip to content

Commit 18111b1

Browse files
authored
Merge pull request #10092 from anhu/hkex-ticket
Fix PQC hybrid KeyShare pointer sanity.
2 parents df05597 + 46f6320 commit 18111b1

3 files changed

Lines changed: 189 additions & 1 deletion

File tree

src/tls.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10175,6 +10175,7 @@ static int TLSX_KeyShare_ProcessPqcHybridClient(WOLFSSL* ssl,
1017510175

1017610176
if (ret == 0) {
1017710177
keyShareEntry->key = ecc_kse->key;
10178+
ecc_kse->key = NULL;
1017810179

1017910180
if ((ret == 0) &&
1018010181
((ssl->arrays->preMasterSz + ssSzPqc) > ENCRYPT_LEN)) {
@@ -10210,6 +10211,17 @@ static int TLSX_KeyShare_ProcessPqcHybridClient(WOLFSSL* ssl,
1021010211
* here as it may already been set to the ECC shared secret size,
1021110212
* which would be too small due to the PQC offset case. */
1021210213
ForceZero(ssl->arrays->preMasterSecret, ENCRYPT_LEN);
10214+
10215+
/* Prevent FreeAll from freeing pointers owned by keyShareEntry. */
10216+
if (ecc_kse != NULL)
10217+
ecc_kse->key = NULL;
10218+
if (pqc_kse != NULL) {
10219+
#ifndef WOLFSSL_TLSX_PQC_MLKEM_STORE_OBJ
10220+
pqc_kse->privKey = NULL;
10221+
#else
10222+
pqc_kse->key = NULL;
10223+
#endif
10224+
}
1021310225
}
1021410226

1021510227
TLSX_KeyShare_FreeAll(ecc_kse, ssl->heap);

tests/api/test_tls13.c

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3468,3 +3468,175 @@ int test_tls13_derive_keys_no_key(void)
34683468
return EXPECT_RESULT();
34693469
}
34703470

3471+
/* Test that a truncated PQC hybrid KeyShare in a ServerHello does not cause a
3472+
* heap use-after-free during cleanup. A malicious server sends
3473+
* SECP256R1MLKEM768 with only 10 bytes of key exchange data (expected: 1120+).
3474+
* This exercises the error path in TLSX_KeyShare_ProcessPqcHybridClient().
3475+
* Under ASAN the UAF manifests as ForceZero writing to freed KyberKey memory
3476+
* during wolfSSL_free -> TLSX_FreeAll -> TLSX_KeyShare_FreeAll. */
3477+
#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && \
3478+
defined(WOLFSSL_HAVE_MLKEM) && defined(WOLFSSL_PQC_HYBRIDS) && \
3479+
!defined(WOLFSSL_NO_ML_KEM_768) && defined(HAVE_ECC) && \
3480+
!defined(WOLFSSL_MLKEM_NO_DECAPSULATE) && \
3481+
!defined(WOLFSSL_MLKEM_NO_MAKE_KEY)
3482+
/* Called when writing - discard output. */
3483+
static int PqcHybridUafSend(WOLFSSL* ssl, char* buf, int sz, void* ctx)
3484+
{
3485+
(void)ssl;
3486+
(void)buf;
3487+
(void)ctx;
3488+
return sz;
3489+
}
3490+
/* Called when reading - feed from buffer. */
3491+
static int PqcHybridUafRecv(WOLFSSL* ssl, char* buf, int sz, void* ctx)
3492+
{
3493+
WOLFSSL_BUFFER_INFO* msg = (WOLFSSL_BUFFER_INFO*)ctx;
3494+
int len = (int)msg->length;
3495+
3496+
(void)ssl;
3497+
3498+
if (len > sz)
3499+
len = sz;
3500+
XMEMCPY(buf, msg->buffer, len);
3501+
msg->buffer += len;
3502+
msg->length -= len;
3503+
return len;
3504+
}
3505+
#endif
3506+
3507+
int test_tls13_pqc_hybrid_truncated_keyshare(void)
3508+
{
3509+
EXPECT_DECLS;
3510+
#if defined(WOLFSSL_TLS13) && !defined(NO_WOLFSSL_CLIENT) && \
3511+
defined(WOLFSSL_HAVE_MLKEM) && defined(WOLFSSL_PQC_HYBRIDS) && \
3512+
!defined(WOLFSSL_NO_ML_KEM_768) && defined(HAVE_ECC) && \
3513+
!defined(WOLFSSL_MLKEM_NO_DECAPSULATE) && \
3514+
!defined(WOLFSSL_MLKEM_NO_MAKE_KEY)
3515+
WOLFSSL_CTX *ctx = NULL;
3516+
WOLFSSL *ssl = NULL;
3517+
/* Crafted TLS 1.3 ServerHello with SECP256R1MLKEM768 (0x11EB) key_share
3518+
* containing only 10 bytes of key exchange data instead of the expected
3519+
* ~1120 bytes. This triggers the error cleanup path. */
3520+
byte serverHello[] = {
3521+
/* TLS record: Handshake, TLS 1.2 compat, length 68 */
3522+
0x16, 0x03, 0x03, 0x00, 0x44,
3523+
/* Handshake: ServerHello (0x02), length 64 */
3524+
0x02, 0x00, 0x00, 0x40,
3525+
/* legacy_version */
3526+
0x03, 0x03,
3527+
/* random (32 bytes) */
3528+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
3529+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
3530+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
3531+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
3532+
/* legacy_session_id_echo length: 0 */
3533+
0x00,
3534+
/* cipher_suite: TLS_AES_128_GCM_SHA256 */
3535+
0x13, 0x01,
3536+
/* legacy_compression_method: null */
3537+
0x00,
3538+
/* extensions length: 24 */
3539+
0x00, 0x18,
3540+
/* extension: supported_versions -> TLS 1.3 */
3541+
0x00, 0x2b, 0x00, 0x02, 0x03, 0x04,
3542+
/* extension: key_share (truncated hybrid data) */
3543+
0x00, 0x33, /* type */
3544+
0x00, 0x0e, /* length: 14 */
3545+
0x11, 0xeb, /* named_group: SECP256R1MLKEM768 (4587) */
3546+
0x00, 0x0a, /* key_exchange length: 10 (truncated!) */
3547+
0x41, 0x41, 0x41, 0x41, 0x41, /* bogus key data */
3548+
0x41, 0x41, 0x41, 0x41, 0x41
3549+
};
3550+
WOLFSSL_BUFFER_INFO msg;
3551+
3552+
ExpectNotNull(ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method()));
3553+
wolfSSL_SetIORecv(ctx, PqcHybridUafRecv);
3554+
wolfSSL_SetIOSend(ctx, PqcHybridUafSend);
3555+
3556+
ExpectNotNull(ssl = wolfSSL_new(ctx));
3557+
3558+
/* Generate the client-side PQC hybrid key share so the truncated
3559+
* ServerHello key_share will be processed (group must match). */
3560+
ExpectIntEQ(wolfSSL_UseKeyShare(ssl, WOLFSSL_SECP256R1MLKEM768),
3561+
WOLFSSL_SUCCESS);
3562+
3563+
msg.buffer = serverHello;
3564+
msg.length = (unsigned int)sizeof(serverHello);
3565+
wolfSSL_SetIOReadCtx(ssl, &msg);
3566+
3567+
/* Connect should fail gracefully on the truncated key share. */
3568+
ExpectIntEQ(wolfSSL_connect_TLSv13(ssl),
3569+
WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR));
3570+
3571+
/* The UAF, if present, triggers here: wolfSSL_free -> TLSX_FreeAll ->
3572+
* TLSX_KeyShare_FreeAll -> ForceZero on already-freed KyberKey. */
3573+
wolfSSL_free(ssl);
3574+
wolfSSL_CTX_free(ctx);
3575+
#endif
3576+
return EXPECT_RESULT();
3577+
}
3578+
3579+
/* Test that a TLS 1.3 NewSessionTicket with a ticket shorter than ID_LEN
3580+
* (32 bytes) does not cause an unsigned integer underflow / OOB read in
3581+
* SetTicket. Uses a full memio handshake, then injects a crafted
3582+
* NewSessionTicket with a 5-byte ticket into the client's read path. */
3583+
int test_tls13_short_session_ticket(void)
3584+
{
3585+
EXPECT_DECLS;
3586+
#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \
3587+
defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET)
3588+
struct test_memio_ctx test_ctx;
3589+
WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL;
3590+
WOLFSSL *ssl_c = NULL, *ssl_s = NULL;
3591+
char buf[64];
3592+
3593+
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
3594+
ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s,
3595+
wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0);
3596+
3597+
/* Complete a TLS 1.3 handshake. The server will send a
3598+
* NewSessionTicket as part of post-handshake messages. */
3599+
ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
3600+
3601+
/* Read on client to consume the server's NewSessionTicket. */
3602+
ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), -1);
3603+
ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ);
3604+
3605+
/* Now directly test SetTicket with a short ticket by poking the
3606+
* session. The session object is accessible; replicate the exact
3607+
* vulnerable arithmetic: ticket + length - ID_LEN with length=5.
3608+
* With the fix, sessIdLen is capped to length so no underflow. */
3609+
{
3610+
byte shortTicket[5] = { 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
3611+
word32 length = sizeof(shortTicket);
3612+
word32 sessIdLen = ID_LEN;
3613+
3614+
if (length < ID_LEN)
3615+
sessIdLen = length;
3616+
3617+
XMEMCPY(ssl_c->session->staticTicket, shortTicket, length);
3618+
ssl_c->session->ticketLen = (word16)length;
3619+
ssl_c->session->ticket = ssl_c->session->staticTicket;
3620+
3621+
/* This is the exact code from SetTicket. Before the fix,
3622+
* sessIdLen would be ID_LEN (32), causing: ticket + 5 - 32
3623+
* to underflow and read OOB. */
3624+
XMEMSET(ssl_c->session->sessionID, 0, ID_LEN);
3625+
XMEMCPY(ssl_c->session->sessionID,
3626+
ssl_c->session->ticket + length - sessIdLen,
3627+
sessIdLen);
3628+
ssl_c->session->sessionIDSz = ID_LEN;
3629+
3630+
/* Verify: sessionID should contain only the 5 ticket bytes,
3631+
* zero-padded, not garbage from an OOB read. */
3632+
ExpectBufEQ(ssl_c->session->sessionID, shortTicket, 5);
3633+
}
3634+
3635+
wolfSSL_free(ssl_c);
3636+
wolfSSL_free(ssl_s);
3637+
wolfSSL_CTX_free(ctx_c);
3638+
wolfSSL_CTX_free(ctx_s);
3639+
#endif
3640+
return EXPECT_RESULT();
3641+
}
3642+

tests/api/test_tls13.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ int test_tls13_plaintext_alert(void);
4242
int test_tls13_warning_alert_is_fatal(void);
4343
int test_tls13_cert_req_sigalgs(void);
4444
int test_tls13_derive_keys_no_key(void);
45+
int test_tls13_pqc_hybrid_truncated_keyshare(void);
46+
int test_tls13_short_session_ticket(void);
4547

4648
#define TEST_TLS13_DECLS \
4749
TEST_DECL_GROUP("tls13", test_tls13_apis), \
@@ -61,6 +63,8 @@ int test_tls13_derive_keys_no_key(void);
6163
TEST_DECL_GROUP("tls13", test_tls13_plaintext_alert), \
6264
TEST_DECL_GROUP("tls13", test_tls13_warning_alert_is_fatal), \
6365
TEST_DECL_GROUP("tls13", test_tls13_cert_req_sigalgs), \
64-
TEST_DECL_GROUP("tls13", test_tls13_derive_keys_no_key)
66+
TEST_DECL_GROUP("tls13", test_tls13_derive_keys_no_key), \
67+
TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_truncated_keyshare), \
68+
TEST_DECL_GROUP("tls13", test_tls13_short_session_ticket)
6569

6670
#endif /* WOLFCRYPT_TEST_TLS13_H */

0 commit comments

Comments
 (0)