From 3304d20ef793ef5ebad6990b2e85ad64bcc68bbf Mon Sep 17 00:00:00 2001 From: Spongman <1088194+Spongman@users.noreply.github.com> Date: Sat, 27 Jun 2026 01:08:24 +0000 Subject: [PATCH 1/2] tests/api: add DTLS 1.3 server CTX reuse ticket regression test test_dtls13_server_ctx_reuse_ticket reproduces a BAD_STATE_E fatal error that occurs when a WOLFSSL_CTX is reused across successive DTLS 1.3 handshakes after issuing a NewSessionTicket. Root cause: TicketEncCbCtx_ChooseKey() refuses to use a key unless expirary[i] >= now + ticketHint, but only generates a replacement when expirary[i] < now. When ticketHint > WOLFSSL_TICKET_KEY_LIFETIME / 2 (e.g. because wolfSSL_CTX_set_timeout() was called with a large value), both keys can be simultaneously alive yet unable to cover the hint window, hitting the BAD_STATE_E branch and causing wolfSSL_accept() to fail fatally on every handshake after the first. test_dtls13_server_ctx_reuse_no_ticket_ok is the control: the same scenario with TLS 1.3 tickets suppressed passes, isolating the fault to NewSessionTicket issuance tainting the reused CTX. --- tests/api/test_dtls.c | 114 ++++++++++++++++++++++++++++++++++++++++++ tests/api/test_dtls.h | 4 ++ 2 files changed, 118 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index ccacf370baa..1872e67092c 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -6276,3 +6276,117 @@ int test_wolfSSL_set_secret(void) return EXPECT_RESULT(); } +/* Control for test_dtls13_server_ctx_reuse_ticket: identical, except the server + * CTX has TLS1.3 session tickets disabled (wolfSSL_CTX_no_ticket_TLSv13). Both + * handshakes then succeed — proving the failure below is caused specifically by + * server-issued session tickets tainting the reused CTX. */ +int test_dtls13_server_ctx_reuse_no_ticket_ok(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_DTLS13) \ + && defined(WOLFSSL_DTLS_CH_FRAG) && defined(WOLFSSL_HAVE_MLKEM) \ + && defined(WOLFSSL_PQC_HYBRIDS) && defined(HAVE_SESSION_TICKET) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + int group = WOLFSSL_X25519MLKEM768; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method), 0); + /* The only difference from the failing test: suppress server tickets. */ + ExpectIntEQ(wolfSSL_CTX_no_ticket_TLSv13(ctx_s), 0); + ExpectIntEQ(wolfSSL_set_groups(ssl_c, &group, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, group), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_dtls13_allow_ch_frag(ssl_s, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_set_groups(ssl_c, &group, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, group), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_dtls13_allow_ch_frag(ssl_s, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* Repro: a single DTLS 1.3 server WOLFSSL_CTX reused across two successive + * handshakes (each with a large hybrid PQ key share, so the ClientHello + * fragments). Handshake #1 succeeds and the server issues a TLS1.3 + * NewSessionTicket; handshake #2 on the SAME server CTX is then rejected by the + * server at its Finished step. Both handshakes must succeed. */ +int test_dtls13_server_ctx_reuse_ticket(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_DTLS13) \ + && defined(WOLFSSL_DTLS_CH_FRAG) && defined(WOLFSSL_HAVE_MLKEM) \ + && defined(WOLFSSL_PQC_HYBRIDS) && defined(HAVE_SESSION_TICKET) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + int group = WOLFSSL_X25519MLKEM768; + const char *msg = "test"; + int msgSz = (int)XSTRLEN(msg) + 1; + byte buf[64]; + + /* Handshake #1 — fresh contexts. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method), 0); + ExpectIntEQ(wolfSSL_set_groups(ssl_c, &group, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, group), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_dtls13_allow_ch_frag(ssl_s, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + /* Exchange app data so the server flushes its post-handshake + * NewSessionTicket and the client consumes it. */ + ExpectIntEQ(wolfSSL_write(ssl_c, msg, msgSz), msgSz); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), msgSz); + ExpectIntEQ(wolfSSL_write(ssl_s, msg, msgSz), msgSz); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), msgSz); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* Handshake #2 — REUSE the same server (and client) CTX, fresh transport. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_set_groups(ssl_c, &group, 1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, group), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_dtls13_allow_ch_frag(ssl_s, 1), WOLFSSL_SUCCESS); + /* The bug: this returns != 0 because the server rejects its own + * handshake at the Finished step on the reused, ticket-tainted CTX. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_dtls.h b/tests/api/test_dtls.h index 4523cdef1e9..3d41bc214a8 100644 --- a/tests/api/test_dtls.h +++ b/tests/api/test_dtls.h @@ -31,6 +31,8 @@ int test_dtls12_record_length_mismatch(void); int test_dtls12_short_read(void); int test_dtls13_longer_length(void); int test_dtls13_short_read(void); +int test_dtls13_server_ctx_reuse_no_ticket_ok(void); +int test_dtls13_server_ctx_reuse_ticket(void); int test_records_span_network_boundaries(void); int test_dtls_record_cross_boundaries(void); int test_dtls_rtx_across_epoch_change(void); @@ -161,6 +163,8 @@ int test_WOLFSSL_dtls_version_alert(void); TEST_DECL_GROUP("dtls", test_dtls13_min_rtx_interval), \ TEST_DECL_GROUP("dtls", test_dtls13_no_session_id_echo), \ TEST_DECL_GROUP("dtls", test_dtls13_oversized_cert_chain), \ + TEST_DECL_GROUP("dtls", test_dtls13_server_ctx_reuse_no_ticket_ok), \ + TEST_DECL_GROUP("dtls", test_dtls13_server_ctx_reuse_ticket), \ TEST_DECL_GROUP("dtls", test_dtls_set_session_min_downgrade), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_create_free_peer), \ TEST_DECL_GROUP("dtls", test_wolfSSL_dtls_get0_peer), \ From 4249744f982c0371aa97a19c8a2388c63119881b Mon Sep 17 00:00:00 2001 From: Spongman <1088194+Spongman@users.noreply.github.com> Date: Sat, 27 Jun 2026 01:08:24 +0000 Subject: [PATCH 2/2] tests/api: add DTLS 1.3 server CTX reuse ticket regression test test_dtls13_server_ctx_reuse_ticket reproduces a BAD_STATE_E fatal error that occurs when a WOLFSSL_CTX is reused across successive DTLS 1.3 handshakes after issuing a NewSessionTicket. Root cause: TicketEncCbCtx_ChooseKey() refuses to use a key unless expirary[i] >= now + ticketHint, but only generates a replacement when expirary[i] < now. When ticketHint > WOLFSSL_TICKET_KEY_LIFETIME / 2 (e.g. because wolfSSL_CTX_set_timeout() was called with a large value), both keys can be simultaneously alive yet unable to cover the hint window, hitting the BAD_STATE_E branch and causing wolfSSL_accept() to fail fatally on every handshake after the first. test_dtls13_server_ctx_reuse_no_ticket_ok is the control: the same scenario with TLS 1.3 tickets suppressed passes, isolating the fault to NewSessionTicket issuance tainting the reused CTX. --- tests/api/test_dtls.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index 1872e67092c..20e67fb726d 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -6352,6 +6352,11 @@ int test_dtls13_server_ctx_reuse_ticket(void) XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method), 0); + /* Set hint > WOLFSSL_TICKET_KEY_LIFETIME / 2 to trigger the bug in + * TicketEncCbCtx_ChooseKey: both keys become alive but neither covers + * the hint window, returning BAD_STATE_E on the 2nd handshake. */ + ExpectIntEQ(wolfSSL_CTX_set_TicketHint(ctx_s, + WOLFSSL_TICKET_KEY_LIFETIME + 1), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_set_groups(ssl_c, &group, 1), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, group), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_dtls13_allow_ch_frag(ssl_s, 1), WOLFSSL_SUCCESS);