From ee07f118413ec00ab3f0925432f18900617b033e Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Fri, 14 Aug 2026 16:00:42 +0000 Subject: [PATCH] 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 748881bf15..f34e2b09bb 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -14450,12 +14450,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 86d9a5347a..d92f94a187 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -11762,3 +11762,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 fd36d92bc7..f47fee2f72 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -135,6 +135,7 @@ int test_tls13_is_init_finished_want_write(void); int test_tls13_cryptocb_async(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), \ @@ -247,6 +248,7 @@ int test_tls13_send_session_ticket_psk_modes(void); TEST_DECL_GROUP("tls13", test_tls13_is_init_finished_want_write), \ TEST_DECL_GROUP("tls13", test_tls13_cryptocb_async), \ 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 */