From 0ed5a51def09500161b90decbd78e0e15ffcac9f Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 16:00:07 +0000 Subject: [PATCH 1/6] Only send TLS 1.3 NewSessionTicket when the client advertised PSK modes RFC 9846 Section 4.3.9 says psk_key_exchange_modes restricts the PSKs the server may supply through NewSessionTicket, and Section 4.7.1 makes sending a ticket conditional on the ClientHello carrying a suitable extension. The send paths only checked that tickets were enabled. Record the modes from the ClientHello in Options - the extension object is freed with the rest of the handshake state, so wolfSSL_send_SessionTicket() cannot look it up. The automatic path skips the ticket; the explicit API returns MISSING_HANDSHAKE_DATA or PSK_KEY_ERROR. Define WOLFSSL_TLS13_TICKET_NO_PSK_MODES for the old behaviour. Fixes https://github.com/wolfSSL/wolfssl/issues/11126 --- src/tls.c | 10 ++- src/tls13.c | 63 +++++++++++++++++ tests/api/test_tls13.c | 150 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 6 +- wolfssl/internal.h | 6 ++ 5 files changed, 233 insertions(+), 2 deletions(-) diff --git a/src/tls.c b/src/tls.c index 895bcb3b99..947739ea5e 100644 --- a/src/tls.c +++ b/src/tls.c @@ -12911,8 +12911,16 @@ static int TLSX_PskKeModes_Parse(WOLFSSL* ssl, const byte* input, word16 length, byte modes; ret = TLSX_PskKeyModes_Parse_Modes(input, length, msgType, &modes); - if (ret == 0) + if (ret == 0) { +#if defined(HAVE_SESSION_TICKET) && !defined(NO_WOLFSSL_SERVER) + /* Keep the advertised modes for the NewSessionTicket decision. The + * extension object is dropped with the rest of the handshake state + * once the handshake is done. */ + ssl->options.pskKeModes = modes; + ssl->options.pskKeModesRecvd = 1; +#endif ret = TLSX_PskKeyModes_Use(ssl, modes); + } if (ret != 0) { WOLFSSL_ERROR_VERBOSE(ret); diff --git a/src/tls13.c b/src/tls13.c index 115ec8391b..dd5ae1b187 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -71,6 +71,11 @@ * WOLFSSL_TICKET_HAVE_ID: Session tickets include ID default: off * Forced on when WOLFSSL_EARLY_DATA is set. * WOLFSSL_TICKET_NONCE_MALLOC: Dynamically allocate ticket nonce default: off + * WOLFSSL_TLS13_TICKET_NO_PSK_MODES: Send NewSessionTicket even default: off + * when the ClientHello advertised no usable + * psk_key_exchange_modes. Restores the pre-check + * behaviour; RFC 9846 Sections 4.3.9 and 4.7.1 say + * the server should not send such a ticket. * * TLS 1.3 Key Exchange: * HAVE_KEYING_MATERIAL: Export keying material (RFC 8446 7.5) default: off @@ -13811,6 +13816,48 @@ static int ExpectedResumptionSecret(WOLFSSL* ssl) } #endif +/* Check the client advertised a PSK key exchange mode a resumption ticket can + * be used with. + * + * RFC 9846 Section 4.3.9: psk_key_exchange_modes restricts both the PSKs + * offered in the ClientHello and those the server might supply through + * NewSessionTicket, and servers should not send tickets that are incompatible + * with the advertised modes. RFC 9846 Section 4.7.1 makes sending a ticket + * conditional on the client's hello carrying a suitable extension. + * + * ssl The SSL/TLS object. + * returns 0 when a ticket may be sent, MISSING_HANDSHAKE_DATA when the + * extension was not received and PSK_KEY_ERROR when none of the + * advertised modes is usable. + */ +static int CheckTls13TicketPskModes(WOLFSSL* ssl) +{ +#ifndef WOLFSSL_TLS13_TICKET_NO_PSK_MODES + if (!ssl->options.pskKeModesRecvd) { + WOLFSSL_MSG("No psk_key_exchange_modes in ClientHello"); + return MISSING_HANDSHAKE_DATA; + } + + if ((ssl->options.pskKeModes & (1 << PSK_KE)) != 0 + #ifdef HAVE_SUPPORTED_CURVES + && !ssl->options.onlyPskDheKe + #endif + ) { + return 0; + } + if ((ssl->options.pskKeModes & (1 << PSK_DHE_KE)) != 0 && + !ssl->options.noPskDheKe) { + return 0; + } + + WOLFSSL_MSG("No usable psk_key_exchange_modes advertised by client"); + return PSK_KEY_ERROR; +#else + (void)ssl; + return 0; +#endif +} + /* Send New Session Ticket handshake message. * Message contains the information required to perform resumption. * @@ -13835,6 +13882,12 @@ static int SendTls13NewSessionTicket(WOLFSSL* ssl) return 0; } + if (CheckTls13TicketPskModes(ssl) != 0) { + WOLFSSL_MSG("Client advertised no usable PSK key exchange mode; " + "skipping ticket"); + return 0; + } + #ifdef WOLFSSL_DTLS13 if (ssl->options.dtls) idx = Dtls13GetRlHeaderLength(ssl, 1) + DTLS_HANDSHAKE_HEADER_SZ; @@ -17045,17 +17098,27 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) * returns BAD_FUNC_ARG when ssl is NULL, or not using TLS v1.3, * SIDE_ERROR when not a server, * NOT_READY_ERROR when handshake not complete, + * MISSING_HANDSHAKE_DATA when the ClientHello had no + * psk_key_exchange_modes extension, + * PSK_KEY_ERROR when no advertised PSK key exchange mode is usable, * WOLFSSL_FATAL_ERROR when creating or sending message fails, and * WOLFSSL_SUCCESS on success. */ int wolfSSL_send_SessionTicket(WOLFSSL* ssl) { + int ret; + if (ssl == NULL || !IsAtLeastTLSv1_3(ssl->version)) return BAD_FUNC_ARG; if (ssl->options.side == WOLFSSL_CLIENT_END) return SIDE_ERROR; if (ssl->options.handShakeState != HANDSHAKE_DONE) return NOT_READY_ERROR; + ret = CheckTls13TicketPskModes(ssl); + if (ret != 0) { + WOLFSSL_ERROR_VERBOSE(ret); + return ret; + } if ((ssl->error = SendTls13NewSessionTicket(ssl)) != 0) { WOLFSSL_ERROR(ssl->error); diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 2fe353732d..49fa47523f 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9282,3 +9282,153 @@ int test_tls13_pha_status_request(void) #endif return EXPECT_RESULT(); } + +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* Drive a TLS 1.3 handshake up to, but not including, the server's final + * wolfSSL_accept() - the call that runs the NewSessionTicket loop. */ +static int test_tls13_handshake_to_ticket(WOLFSSL* ssl_c, WOLFSSL* ssl_s) +{ + EXPECT_DECLS; + + /* ClientHello. */ + ExpectIntEQ(wolfSSL_connect(ssl_c), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + /* Server flight. */ + ExpectIntEQ(wolfSSL_accept(ssl_s), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_s, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + /* Client Finished. */ + ExpectIntEQ(wolfSSL_connect(ssl_c), WOLFSSL_SUCCESS); + + return EXPECT_RESULT(); +} +#endif + +/* RFC 9846 Section 4.3.9 and Section 4.7.1: a NewSessionTicket creates a + * resumption PSK, so the server may only send one when the ClientHello + * advertised a psk_key_exchange_modes mode it can be used with. */ +int test_tls13_ticket_psk_modes(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + /* A ClientHello without psk_key_exchange_modes gets no ticket. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_tls13_handshake_to_ticket(ssl_c, ssl_s), TEST_SUCCESS); + ExpectIntEQ(ssl_s->options.pskKeModesRecvd, 1); + if (EXPECT_SUCCESS()) { + ssl_s->options.pskKeModesRecvd = 0; + } + ExpectIntEQ(test_ctx.c_len, 0); + ExpectIntEQ(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(test_ctx.c_len, 0); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + wolfSSL_CTX_free(ctx_c); + ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); + ctx_s = NULL; + + /* Control: the wolfSSL client advertises the modes, so a ticket is sent. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_tls13_handshake_to_ticket(ssl_c, ssl_s), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_len, 0); + ExpectIntEQ(wolfSSL_accept(ssl_s), WOLFSSL_SUCCESS); + ExpectIntGT(test_ctx.c_len, 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* wolfSSL_send_SessionTicket() applies the same RFC 9846 precondition as the + * automatic ticket path, and reports why it will not send. */ +int test_tls13_send_session_ticket_psk_modes(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), WOLFSSL_SUCCESS); + if (EXPECT_SUCCESS()) { + /* No psk_key_exchange_modes extension in the ClientHello. */ + ssl_s->options.pskKeModesRecvd = 0; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), + WC_NO_ERR_TRACE(MISSING_HANDSHAKE_DATA)); + if (EXPECT_SUCCESS()) { + /* Extension present but carrying only unrecognized modes. */ + ssl_s->options.pskKeModesRecvd = 1; + ssl_s->options.pskKeModes = 0; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + if (EXPECT_SUCCESS()) { + /* psk_dhe_ke only, but the server refuses (EC)DHE with PSK. */ + ssl_s->options.pskKeModes = 1 << PSK_DHE_KE; + ssl_s->options.noPskDheKe = 1; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + if (EXPECT_SUCCESS()) { + ssl_s->options.noPskDheKe = 0; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), WOLFSSL_SUCCESS); +#ifdef HAVE_SUPPORTED_CURVES + if (EXPECT_SUCCESS()) { + /* psk_ke only, but the server requires (EC)DHE with PSK. */ + ssl_s->options.pskKeModes = 1 << PSK_KE; + ssl_s->options.onlyPskDheKe = 1; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), + WC_NO_ERR_TRACE(PSK_KEY_ERROR)); + if (EXPECT_SUCCESS()) { + ssl_s->options.onlyPskDheKe = 0; + } + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), WOLFSSL_SUCCESS); +#endif + + 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_tls13.h b/tests/api/test_tls13.h index 707f3b8771..46bb7b02d4 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -109,6 +109,8 @@ int test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256(void); int test_tls13_KeyUpdate_sender_limit(void); int test_tls13_pqc_hybrid_async_server(void); int test_tls13_pha_status_request(void); +int test_tls13_ticket_psk_modes(void); +int test_tls13_send_session_ticket_psk_modes(void); #define TEST_TLS13_DECLS \ TEST_DECL_GROUP("tls13", test_tls13_apis), \ @@ -195,6 +197,8 @@ int test_tls13_pha_status_request(void); TEST_DECL_GROUP("tls13", test_tls13_AEAD_limit_KU_aes128_ccm_8_sha256), \ TEST_DECL_GROUP("tls13", test_tls13_KeyUpdate_sender_limit), \ TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_async_server), \ - TEST_DECL_GROUP("tls13", test_tls13_pha_status_request) + TEST_DECL_GROUP("tls13", test_tls13_pha_status_request), \ + TEST_DECL_GROUP("tls13", test_tls13_ticket_psk_modes), \ + TEST_DECL_GROUP("tls13", test_tls13_send_session_ticket_psk_modes) #endif /* WOLFCRYPT_TEST_TLS13_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 222ac75c3f..06b7c6159d 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5386,6 +5386,9 @@ struct Options { #if defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TLS13) unsigned int maxTicketTls13; /* maximum number of tickets to send */ unsigned int ticketsSent; /* keep track of the total sent */ +#ifndef NO_WOLFSSL_SERVER + byte pskKeModes; /* modes client advertised in CH */ +#endif #endif /* on/off or small bit flags, optimize layout */ @@ -5465,6 +5468,9 @@ struct Options { word16 noTicketTls12:1; /* TLS 1.2 server won't send ticket */ #ifdef WOLFSSL_TLS13 word16 noTicketTls13:1; /* Server won't create new Ticket */ +#ifndef NO_WOLFSSL_SERVER + word16 pskKeModesRecvd:1; /* CH had psk_key_exchange_modes */ +#endif #endif #endif #ifdef WOLFSSL_DTLS From 534b9a974cafa274f619944fbd6a6afb3e4cb1e7 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 16:00:42 +0000 Subject: [PATCH 2/6] Check NewSessionTicket extension framing without early data RFC 9846 Section 4.7.1 defines NewSessionTicket.extensions as a list of Section 4.3 Extension TLVs, and Section 6 requires a decode_error alert for a message that cannot be parsed. DoTls13NewSessionTicket() only validated the outer vector length unless WOLFSSL_EARLY_DATA was defined, so a client built without early data accepted malformed framing. Fixes https://github.com/wolfSSL/wolfssl/issues/11127 --- src/tls13.c | 8 +-- tests/api/test_tls13.c | 139 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 4 +- 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/src/tls13.c b/src/tls13.c index dd5ae1b187..6b9bc7177b 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -13671,12 +13671,12 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, *inOutIdx += EXTS_SZ; if ((*inOutIdx - begin) + length != size) return BUFFER_ERROR; - #ifdef WOLFSSL_EARLY_DATA - ret = TLSX_Parse(ssl, (byte *)input + (*inOutIdx), length, session_ticket, - NULL); + /* RFC 9846 Section 4.7.1: the extensions are Section 4.3 Extension TLVs. + * Malformed framing is a syntax error even when no extension in the list + * is one we act on. */ + ret = TLSX_Parse(ssl, input + *inOutIdx, length, session_ticket, NULL); if (ret != 0) return ret; - #endif *inOutIdx += length; SetupSession(ssl); diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 49fa47523f..dec74c31a8 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9432,3 +9432,142 @@ int test_tls13_send_session_ticket_psk_modes(void) #endif return EXPECT_RESULT(); } + +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +/* Build a NewSessionTicket handshake message carrying the given extensions + * block verbatim. Returns the message length, including the handshake + * header, or -1 when it does not fit. */ +static int test_tls13_make_nst(byte* out, int outSz, const byte* exts, + int extsSz) +{ + static const byte body[] = { + 0x00, 0x00, 0x0e, 0x10, /* ticket_lifetime: 3600 */ + 0x01, 0x02, 0x03, 0x04, /* ticket_age_add */ + 0x01, 0x00, /* ticket_nonce<1> */ + 0x00, 0x04, 0xde, 0xad, 0xbe, 0xef /* ticket<4> */ + }; + int len = (int)sizeof(body) + OPAQUE16_LEN + extsSz; + + if (outSz < HANDSHAKE_HEADER_SZ + len) + return -1; + + out[0] = session_ticket; + out[1] = (byte)(len >> 16); + out[2] = (byte)(len >> 8); + out[3] = (byte)len; + XMEMCPY(out + HANDSHAKE_HEADER_SZ, body, sizeof(body)); + c16toa((word16)extsSz, out + HANDSHAKE_HEADER_SZ + sizeof(body)); + if (extsSz > 0) { + XMEMCPY(out + HANDSHAKE_HEADER_SZ + sizeof(body) + OPAQUE16_LEN, exts, + (size_t)extsSz); + } + + return HANDSHAKE_HEADER_SZ + len; +} + +/* Encrypt a post-handshake message with the server's keys and hand it to the + * client. Returns 0 on success. */ +static int test_tls13_send_post_hs(struct test_memio_ctx* test_ctx, + WOLFSSL* ssl_s, const byte* msg, int msgSz) +{ + EXPECT_DECLS; + byte rec[256]; + int recSz; + + recSz = BuildTls13Message(ssl_s, rec, (int)sizeof(rec), msg, msgSz, + handshake, 0, 0, 0); + ExpectIntGT(recSz, 0); + ExpectIntLE(recSz, (int)sizeof(rec)); + ExpectIntEQ(test_memio_inject_message(test_ctx, 1, (const char*)rec, recSz), + 0); + + return EXPECT_RESULT(); +} +#endif + +/* RFC 9846 Section 4.7.1 defines NewSessionTicket.extensions as a list of + * Section 4.3 Extension TLVs, and Section 6 requires a decode_error alert for + * a message that cannot be parsed. The framing has to be checked whether or + * not early data - the only extension wolfSSL acts on there - is compiled in. + */ +int test_tls13_new_session_ticket_ext_framing(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + /* Well formed extension of an unknown type - must be ignored. */ + static const byte extOk[] = { 0x12, 0x34, 0x00, 0x02, 0xaa, 0xbb }; + /* Vector too short to hold an Extension header. */ + static const byte extShort[] = { 0x00, 0x2a, 0x00 }; + /* extension_data length runs past the end of the vector. */ + static const byte extTrunc[] = { 0x12, 0x34, 0x00, 0x04, 0xaa, 0xbb }; + struct { + const byte* exts; + int extsSz; + int expectErr; + } cases[] = { + { extOk, (int)sizeof(extOk), 0 }, + { extShort, (int)sizeof(extShort), BUFFER_ERROR }, + { extTrunc, (int)sizeof(extTrunc), BUFFER_ERROR }, + }; + size_t i; + char buf[64]; + + for (i = 0; i < XELEM_CNT(cases) && EXPECT_SUCCESS(); i++) { + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + struct test_memio_ctx test_ctx; + WOLFSSL_ALERT_HISTORY h; + byte msg[64]; + int msgSz; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Consume the server's own NewSessionTicket. */ + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + + msgSz = -1; + if (EXPECT_SUCCESS()) { + msgSz = test_tls13_make_nst(msg, (int)sizeof(msg), cases[i].exts, + cases[i].extsSz); + } + ExpectIntGT(msgSz, 0); + ExpectIntEQ(test_tls13_send_post_hs(&test_ctx, ssl_s, msg, msgSz), + TEST_SUCCESS); + + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), + WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + if (cases[i].expectErr == 0) { + /* Accepted: no application data follows the ticket. */ + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WOLFSSL_ERROR_WANT_READ); + } + else { + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + cases[i].expectErr); + ExpectIntEQ(wolfSSL_get_alert_history(ssl_c, &h), WOLFSSL_SUCCESS); + ExpectIntEQ(h.last_tx.code, decode_error); + ExpectIntEQ(h.last_tx.level, alert_fatal); + } + + 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_tls13.h b/tests/api/test_tls13.h index 46bb7b02d4..feeefcfc6a 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -111,6 +111,7 @@ int test_tls13_pqc_hybrid_async_server(void); int test_tls13_pha_status_request(void); int test_tls13_ticket_psk_modes(void); int test_tls13_send_session_ticket_psk_modes(void); +int test_tls13_new_session_ticket_ext_framing(void); #define TEST_TLS13_DECLS \ TEST_DECL_GROUP("tls13", test_tls13_apis), \ @@ -199,6 +200,7 @@ int test_tls13_send_session_ticket_psk_modes(void); TEST_DECL_GROUP("tls13", test_tls13_pqc_hybrid_async_server), \ TEST_DECL_GROUP("tls13", test_tls13_pha_status_request), \ TEST_DECL_GROUP("tls13", test_tls13_ticket_psk_modes), \ - TEST_DECL_GROUP("tls13", test_tls13_send_session_ticket_psk_modes) + TEST_DECL_GROUP("tls13", test_tls13_send_session_ticket_psk_modes), \ + TEST_DECL_GROUP("tls13", test_tls13_new_session_ticket_ext_framing) #endif /* WOLFCRYPT_TEST_TLS13_H */ From ef6c03f26b1221f00f748e971d0358bf53f634c0 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 16:01:21 +0000 Subject: [PATCH 3/6] Match the full server ID length in the client session cache wolfSSL_GetSessionClient() compared only the requested number of bytes, so a short server ID could match a cached entry that merely started with the same bytes and landed in the same cache row. Applications partitioning the cache with wolfSSL_SetServerID() could then offer a ticket across partitions, against RFC 9846 Appendix C.4. Fixes https://github.com/wolfSSL/wolfssl/issues/11132 --- src/ssl_sess.c | 7 +++- tests/api/test_session.c | 79 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_session.h | 4 +- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/ssl_sess.c b/src/ssl_sess.c index 862a7afbd9..c70feefd08 100644 --- a/src/ssl_sess.c +++ b/src/ssl_sess.c @@ -1052,8 +1052,11 @@ WOLFSSL_SESSION* wolfSSL_GetSessionClient(WOLFSSL* ssl, const byte* id, int len) #else current = &sessRow->Sessions[clSess[idx].serverIdx]; #endif - if (current && XMEMCMP(current->serverID, id, - (unsigned long)len) == 0) { + /* Require the same length as well as the same bytes. Comparing only + * the requested length lets a short ID alias the prefix of a longer + * cached one, mixing sessions the application meant to keep apart. */ + if (current && current->idLen == (word16)len && + XMEMCMP(current->serverID, id, (unsigned long)len) == 0) { WOLFSSL_MSG("Found a serverid match for client"); if (LowResTimer() < (current->bornOn + current->timeout)) { WOLFSSL_MSG("Session valid"); diff --git a/tests/api/test_session.c b/tests/api/test_session.c index 3c08f32bef..5ad90c8eb8 100644 --- a/tests/api/test_session.c +++ b/tests/api/test_session.c @@ -1739,3 +1739,82 @@ int test_wolfSSL_GetSessionAtIndex(void) #endif /* SESSION_INDEX && HAVE_SESSION_TICKET && !NO_SESSION_CACHE && * !NO_WOLFSSL_CLIENT && !NO_TLS */ + +/* RFC 9846 Appendix C.4: client applications should not offer tickets across + * connections meant to be uncorrelated. wolfSSL_SetServerID() is how an + * application keeps such connections apart, so a shorter ID must not match a + * cached entry that merely starts with the same bytes. */ +int test_wolfSSL_client_cache_id_prefix(void) +{ + EXPECT_DECLS; +#if !defined(NO_SESSION_CACHE) && !defined(NO_CLIENT_CACHE) && \ + !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL* ssl = NULL; + struct test_memio_ctx test_ctx; + static const byte prefix[] = { 'w', 'o', 'l', 'f', 'S', 'S', 'L', ':' }; + byte id[sizeof(prefix) + 4]; + byte sessId[ID_LEN]; + word32 i; + /* The cache row is picked from a hash of the ID, so the prefix and any + * one long ID rarely share a row. Every long ID here starts with the + * prefix, and there are enough of them to cover all rows, so the prefix + * lookup lands on a row holding one of them. */ + const word32 fill = 4096; + + /* TLS 1.2 so the client has a complete session to cache as soon as the + * handshake is done, with or without session tickets. */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_c->session->isSetup, 1); + + XMEMCPY(id, prefix, sizeof(prefix)); + XMEMSET(sessId, 0, sizeof(sessId)); + for (i = 0; i < fill && EXPECT_SUCCESS(); i++) { + ClientSession* entry = NULL; + + c32toa(i, id + sizeof(prefix)); + c32toa(i, sessId); + ExpectIntEQ(wolfSSL_SetServerID(ssl_c, id, (int)sizeof(id), 1), + WOLFSSL_SUCCESS); + if (EXPECT_SUCCESS()) { + XMEMCPY(ssl_c->session->sessionID, sessId, ID_LEN); + XMEMCPY(ssl_c->session->altSessionID, sessId, ID_LEN); + ssl_c->session->sessionIDSz = ID_LEN; + } + ExpectIntEQ(AddSessionToCache(ctx_c, ssl_c->session, sessId, ID_LEN, + NULL, WOLFSSL_CLIENT_END, 0, &entry), 0); + ExpectNotNull(entry); + } + + /* The prefix is a distinct partition key: no cached session for it. */ + ExpectNotNull(ssl = wolfSSL_new(ctx_c)); + ExpectIntEQ(ssl->session->isSetup, 0); + ExpectIntEQ(wolfSSL_SetServerID(ssl, prefix, (int)sizeof(prefix), 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(ssl->session->isSetup, 0); + ExpectIntEQ(ssl->session->idLen, (int)sizeof(prefix)); + wolfSSL_free(ssl); + ssl = NULL; + + /* Control: the ID it was cached under still finds it. */ + ExpectNotNull(ssl = wolfSSL_new(ctx_c)); + ExpectIntEQ(wolfSSL_SetServerID(ssl, id, (int)sizeof(id), 0), + WOLFSSL_SUCCESS); + ExpectIntEQ(ssl->session->isSetup, 1); + wolfSSL_free(ssl); + + 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_session.h b/tests/api/test_session.h index be3deb5f04..8781fd8ac4 100644 --- a/tests/api/test_session.h +++ b/tests/api/test_session.h @@ -37,6 +37,7 @@ int test_wolfSSL_CTX_sess_set_remove_cb(void); int test_wolfSSL_ticket_keys(void); int test_wolfSSL_SESSION_get_ex_new_index(void); int test_wolfSSL_GetSessionAtIndex(void); +int test_wolfSSL_client_cache_id_prefix(void); #define TEST_SESSION_DECLS \ TEST_DECL_GROUP("session", test_wolfSSL_CTX_add_session), \ @@ -51,6 +52,7 @@ int test_wolfSSL_GetSessionAtIndex(void); TEST_DECL_GROUP("session", test_wolfSSL_CTX_sess_set_remove_cb), \ TEST_DECL_GROUP("session", test_wolfSSL_ticket_keys), \ TEST_DECL_GROUP("session", test_wolfSSL_SESSION_get_ex_new_index), \ - TEST_DECL_GROUP("session", test_wolfSSL_GetSessionAtIndex) + TEST_DECL_GROUP("session", test_wolfSSL_GetSessionAtIndex), \ + TEST_DECL_GROUP("session", test_wolfSSL_client_cache_id_prefix) #endif /* WOLFCRYPT_TEST_SESSION_H */ From 7072ea628407aa23236514e34564d492dc0412f8 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 18:20:39 +0000 Subject: [PATCH 4/6] Make the NewSessionTicket PSK mode check opt-in Withholding a ticket from a client that omits psk_key_exchange_modes is a behaviour change for peers that omit the extension but still expect a ticket. Keep the old behaviour by default and gate the RFC 9846 check behind WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES, including the Options fields that record the advertised modes. --- .wolfssl_known_macro_extras | 1 + src/tls.c | 3 ++- src/tls13.c | 17 ++++++++++------- tests/api/test_tls13.c | 6 +++--- wolfssl/internal.h | 6 ++++-- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index 3be2bfaa0a..3b9b9b3a99 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1096,6 +1096,7 @@ WOLFSSL_TLS13_DRAFT WOLFSSL_TLS13_IGNORE_AEAD_LIMITS WOLFSSL_TLS13_IGNORE_PT_ALERT_ON_ENC WOLFSSL_TLS13_SHA512 +WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES WOLFSSL_TLS13_TICKET_BEFORE_FINISHED WOLFSSL_TLSX_PQC_MLKEM_STORE_PRIV_KEY WOLFSSL_TRACK_MEMORY_FULL diff --git a/src/tls.c b/src/tls.c index 947739ea5e..bf313c5c6e 100644 --- a/src/tls.c +++ b/src/tls.c @@ -12912,7 +12912,8 @@ static int TLSX_PskKeModes_Parse(WOLFSSL* ssl, const byte* input, word16 length, ret = TLSX_PskKeyModes_Parse_Modes(input, length, msgType, &modes); if (ret == 0) { -#if defined(HAVE_SESSION_TICKET) && !defined(NO_WOLFSSL_SERVER) +#if defined(HAVE_SESSION_TICKET) && !defined(NO_WOLFSSL_SERVER) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) /* Keep the advertised modes for the NewSessionTicket decision. The * extension object is dropped with the rest of the handshake state * once the handshake is done. */ diff --git a/src/tls13.c b/src/tls13.c index 6b9bc7177b..0cadbc91c8 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -71,11 +71,12 @@ * WOLFSSL_TICKET_HAVE_ID: Session tickets include ID default: off * Forced on when WOLFSSL_EARLY_DATA is set. * WOLFSSL_TICKET_NONCE_MALLOC: Dynamically allocate ticket nonce default: off - * WOLFSSL_TLS13_TICKET_NO_PSK_MODES: Send NewSessionTicket even default: off + * WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES: Withhold NewSessionTicket default: off * when the ClientHello advertised no usable - * psk_key_exchange_modes. Restores the pre-check - * behaviour; RFC 9846 Sections 4.3.9 and 4.7.1 say - * the server should not send such a ticket. + * psk_key_exchange_modes, as RFC 9846 Sections + * 4.3.9 and 4.7.1 require. Off by default: a peer + * that omits the extension but expects a ticket + * stops getting one. * * TLS 1.3 Key Exchange: * HAVE_KEYING_MATERIAL: Export keying material (RFC 8446 7.5) default: off @@ -13832,7 +13833,7 @@ static int ExpectedResumptionSecret(WOLFSSL* ssl) */ static int CheckTls13TicketPskModes(WOLFSSL* ssl) { -#ifndef WOLFSSL_TLS13_TICKET_NO_PSK_MODES +#ifdef WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES if (!ssl->options.pskKeModesRecvd) { WOLFSSL_MSG("No psk_key_exchange_modes in ClientHello"); return MISSING_HANDSHAKE_DATA; @@ -17099,8 +17100,10 @@ int wolfSSL_accept_TLSv13(WOLFSSL* ssl) * SIDE_ERROR when not a server, * NOT_READY_ERROR when handshake not complete, * MISSING_HANDSHAKE_DATA when the ClientHello had no - * psk_key_exchange_modes extension, - * PSK_KEY_ERROR when no advertised PSK key exchange mode is usable, + * psk_key_exchange_modes extension and + * WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES is defined, + * PSK_KEY_ERROR when no advertised PSK key exchange mode is usable and + * WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES is defined, * WOLFSSL_FATAL_ERROR when creating or sending message fails, and * WOLFSSL_SUCCESS on success. */ diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index dec74c31a8..1f98da3725 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9286,7 +9286,7 @@ int test_tls13_pha_status_request(void) #if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ - !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) /* Drive a TLS 1.3 handshake up to, but not including, the server's final * wolfSSL_accept() - the call that runs the NewSessionTicket loop. */ @@ -9318,7 +9318,7 @@ int test_tls13_ticket_psk_modes(void) #if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ - !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) WOLFSSL_CTX* ctx_c = NULL; WOLFSSL_CTX* ctx_s = NULL; @@ -9373,7 +9373,7 @@ int test_tls13_send_session_ticket_psk_modes(void) #if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && \ - !defined(WOLFSSL_TLS13_TICKET_NO_PSK_MODES) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) && \ !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) WOLFSSL_CTX* ctx_c = NULL; WOLFSSL_CTX* ctx_s = NULL; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 06b7c6159d..da3b740230 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -5386,7 +5386,8 @@ struct Options { #if defined(HAVE_SESSION_TICKET) && defined(WOLFSSL_TLS13) unsigned int maxTicketTls13; /* maximum number of tickets to send */ unsigned int ticketsSent; /* keep track of the total sent */ -#ifndef NO_WOLFSSL_SERVER +#if !defined(NO_WOLFSSL_SERVER) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) byte pskKeModes; /* modes client advertised in CH */ #endif #endif @@ -5468,7 +5469,8 @@ struct Options { word16 noTicketTls12:1; /* TLS 1.2 server won't send ticket */ #ifdef WOLFSSL_TLS13 word16 noTicketTls13:1; /* Server won't create new Ticket */ -#ifndef NO_WOLFSSL_SERVER +#if !defined(NO_WOLFSSL_SERVER) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) word16 pskKeModesRecvd:1; /* CH had psk_key_exchange_modes */ #endif #endif From 063b9c5706f8b442f4401a8beef2a29b584181be Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 18:24:19 +0000 Subject: [PATCH 5/6] Assert unrecognized NewSessionTicket extensions keep the connection up RFC 9846 Section 4.7.1 requires clients to ignore unrecognized extensions, so cover an empty vector, one unknown type and two unknown types, and check that application data still flows both ways afterwards with no alert sent. These cases pass with or without the framing check and pin down what it must not reject. --- tests/api/test_tls13.c | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 1f98da3725..0b57dd0ac7 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9502,16 +9502,22 @@ int test_tls13_new_session_ticket_ext_framing(void) !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) /* Well formed extension of an unknown type - must be ignored. */ static const byte extOk[] = { 0x12, 0x34, 0x00, 0x02, 0xaa, 0xbb }; + /* Two unknown types, the second with empty extension_data. */ + static const byte extTwo[] = { 0x12, 0x34, 0x00, 0x02, 0xaa, 0xbb, + 0x56, 0x78, 0x00, 0x00 }; /* Vector too short to hold an Extension header. */ static const byte extShort[] = { 0x00, 0x2a, 0x00 }; /* extension_data length runs past the end of the vector. */ static const byte extTrunc[] = { 0x12, 0x34, 0x00, 0x04, 0xaa, 0xbb }; + static const char appData[] = "still talking"; struct { const byte* exts; int extsSz; int expectErr; } cases[] = { + { NULL, 0, 0 }, { extOk, (int)sizeof(extOk), 0 }, + { extTwo, (int)sizeof(extTwo), 0 }, { extShort, (int)sizeof(extShort), BUFFER_ERROR }, { extTrunc, (int)sizeof(extTrunc), BUFFER_ERROR }, }; @@ -9551,9 +9557,24 @@ int test_tls13_new_session_ticket_ext_framing(void) ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); if (cases[i].expectErr == 0) { - /* Accepted: no application data follows the ticket. */ + /* Accepted: no application data follows the ticket, and the + * client raised no alert. */ ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(wolfSSL_get_alert_history(ssl_c, &h), WOLFSSL_SUCCESS); + ExpectIntEQ(h.last_tx.code, -1); + ExpectIntEQ(h.last_tx.level, -1); + /* The connection carries on: data still flows both ways. */ + ExpectIntEQ(wolfSSL_write(ssl_s, appData, (int)sizeof(appData) - 1), + (int)sizeof(appData) - 1); + ExpectIntEQ(wolfSSL_read(ssl_c, buf, sizeof(buf)), + (int)sizeof(appData) - 1); + ExpectIntEQ(XMEMCMP(buf, appData, sizeof(appData) - 1), 0); + ExpectIntEQ(wolfSSL_write(ssl_c, appData, (int)sizeof(appData) - 1), + (int)sizeof(appData) - 1); + ExpectIntEQ(wolfSSL_read(ssl_s, buf, sizeof(buf)), + (int)sizeof(appData) - 1); + ExpectIntEQ(XMEMCMP(buf, appData, sizeof(appData) - 1), 0); } else { ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), From 973d8e2fe14fe1667da127b77fe930c138bdb1ed Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 17 Aug 2026 03:24:40 +0000 Subject: [PATCH 6/6] Address review: clear the recorded PSK modes on object reuse wolfSSL_clear() reset ticketsSent but left pskKeModes/pskKeModesRecvd, both recorded from the ClientHello. A reused server object would judge the next connection on the previous one's advertised modes. Also correct the test comment: the wolfSSL client does advertise psk_key_exchange_modes, so the no-extension case is driven by clearing the recorded flag after the handshake. --- src/ssl.c | 7 +++++++ tests/api/test_tls13.c | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ssl.c b/src/ssl.c index 2e3b49452e..822d1e59b9 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5719,6 +5719,13 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, #ifdef HAVE_SESSION_TICKET #ifdef WOLFSSL_TLS13 ssl->options.ticketsSent = 0; + #if !defined(NO_WOLFSSL_SERVER) && \ + defined(WOLFSSL_TLS13_TICKET_CHECK_PSK_MODES) + /* Recorded from the ClientHello, so it must not carry into the next + * connection on a reused object. */ + ssl->options.pskKeModes = 0; + ssl->options.pskKeModesRecvd = 0; + #endif #endif ssl->options.rejectTicket = 0; #endif diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index 0b57dd0ac7..2308a24444 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -9326,7 +9326,9 @@ int test_tls13_ticket_psk_modes(void) WOLFSSL* ssl_s = NULL; struct test_memio_ctx test_ctx; - /* A ClientHello without psk_key_exchange_modes gets no ticket. */ + /* The wolfSSL client always advertises psk_key_exchange_modes, so drive + * the no-extension case by clearing the recorded flag after the + * handshake: a ClientHello without the extension gets no ticket. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0);