From c1a255c27f7d824a73b91e4aabe54c178ea8ff59 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 29 Sep 2026 07:56:59 -0500 Subject: [PATCH 1/2] Fragment TLS 1.3 NewSessionTicket to max fragment length --- src/internal.c | 5 +- src/tls13.c | 63 +++++++++++++++++++---- tests/api/test_tls13.c | 110 +++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls13.h | 4 +- 4 files changed, 169 insertions(+), 13 deletions(-) diff --git a/src/internal.c b/src/internal.c index 1f62c435246..8279825a650 100644 --- a/src/internal.c +++ b/src/internal.c @@ -44643,8 +44643,9 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) if (certDer != NULL && certDerSz > 0 && certDerSz <= MAX_TICKET_PEER_CERT_SZ #ifdef HAVE_MAX_FRAGMENT - /* We don't support fragmentation in - * SendTls13NewSessionTicket yet. */ + /* The ticket returns in a ClientHello, which is never + * fragmented and after a HelloRetryRequest must fit the + * reduced limit. */ && (!IsAtLeastTLSv1_3(ssl->version) || ssl->max_fragment == MAX_RECORD_SIZE) #endif diff --git a/src/tls13.c b/src/tls13.c index 7cf731a3d2f..b7f4da9ebff 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -15003,6 +15003,12 @@ static int SendTls13NewSessionTicket(WOLFSSL* ssl) word16 extSz; int genAltSessionID; word32 idx = RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ; + word32 hsSz = 0; + word32 fragSz; + word32 fragOff; + word32 fragRoom = 0; + int maxFrag = 0; + byte* plain; WOLFSSL_START(WC_FUNC_NEW_SESSION_TICKET_SEND); WOLFSSL_ENTER("SendTls13NewSessionTicket"); @@ -15107,12 +15113,27 @@ static int SendTls13NewSessionTicket(WOLFSSL* ssl) sendSz = (int)(idx + length + MAX_MSG_EXTRA); + /* DTLS 1.3 fragments in Dtls13HandshakeSend(). Otherwise reserve room + * for the records ahead of the plaintext so neither overlaps. */ + if (!ssl->options.dtls) { + maxFrag = wolfssl_local_GetMaxPlaintextSize(ssl); + if (maxFrag <= 0) + return (maxFrag < 0) ? maxFrag : BUFFER_E; + hsSz = HANDSHAKE_HEADER_SZ + length; + if (hsSz > (word32)maxFrag) { + fragRoom = hsSz + ((hsSz + (word32)maxFrag - 1) / + (word32)maxFrag) * + (RECORD_HEADER_SZ + MAX_MSG_EXTRA); + sendSz = (int)(fragRoom + idx + length); + } + } + /* Check buffers are big enough and grow if needed. */ if ((ret = CheckAvailableSize(ssl, sendSz)) != 0) return ret; /* Get position in output buffer to write new message to. */ - output = GetOutputBuffer(ssl); + output = GetOutputBuffer(ssl) + fragRoom; /* Put the record and handshake headers on. */ AddTls13Headers(output, length, session_ticket, ssl); @@ -15162,7 +15183,7 @@ static int SendTls13NewSessionTicket(WOLFSSL* ssl) #endif if (idx > WOLFSSL_MAX_16BIT || - sendSz > (int)WOLFSSL_MAX_16BIT) { + (fragRoom == 0 && sendSz > (int)WOLFSSL_MAX_16BIT)) { return BAD_LENGTH_E; } @@ -15182,15 +15203,37 @@ static int SendTls13NewSessionTicket(WOLFSSL* ssl) (word16)idx, session_ticket, 0); #endif /* WOLFSSL_DTLS13 */ - /* This message is always encrypted. */ - sendSz = BuildTls13Message(ssl, output, sendSz, - output + RECORD_HEADER_SZ, - (word16)idx - RECORD_HEADER_SZ, - handshake, 0, 0, 0); - if (sendSz < 0) - return sendSz; + if (fragRoom > 0) { + /* RFC 8446 Section 5.1: split across adjacent handshake records so + * each fits the negotiated maximum fragment length. */ + plain = output + RECORD_HEADER_SZ; + for (fragOff = 0; fragOff < hsSz; fragOff += fragSz) { + fragSz = min(hsSz - fragOff, (word32)maxFrag); + output = GetOutputBuffer(ssl); + sendSz = BuildTls13Message(ssl, output, + (int)(RECORD_HEADER_SZ + fragSz + + MAX_MSG_EXTRA), + plain + fragOff, (int)fragSz, + handshake, 0, 0, 0); + if (sendSz < 0) + break; + ssl->buffers.outputBuffer.length += (word32)sendSz; + } + ForceZero(plain, hsSz); + if (sendSz < 0) + return sendSz; + } + else { + /* This message is always encrypted. */ + sendSz = BuildTls13Message(ssl, output, sendSz, + output + RECORD_HEADER_SZ, + (word16)idx - RECORD_HEADER_SZ, + handshake, 0, 0, 0); + if (sendSz < 0) + return sendSz; - ssl->buffers.outputBuffer.length += sendSz; + ssl->buffers.outputBuffer.length += sendSz; + } /* Always send as this is either directly after server's Finished or only * message after client's Finished. diff --git a/tests/api/test_tls13.c b/tests/api/test_tls13.c index cb18b253316..3b4a26f8df9 100644 --- a/tests/api/test_tls13.c +++ b/tests/api/test_tls13.c @@ -17370,3 +17370,113 @@ int test_tls13_cks_hrr_reparse(void) #endif return EXPECT_RESULT(); } + +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MAX_FRAGMENT) +/* Ticket encryption callback that pads every ticket to the largest size the + * server allows, so the NewSessionTicket length does not depend on the + * build. */ +static int test_tls13_nst_max_frag_ticketEncCb(WOLFSSL* ssl, + byte key_name[WOLFSSL_TICKET_NAME_SZ], byte iv[WOLFSSL_TICKET_IV_SZ], + byte mac[WOLFSSL_TICKET_MAC_SZ], int enc, byte* ticket, int inLen, + int* outLen, void* userCtx) +{ + int i; + + (void)ssl; + (void)userCtx; + + if (enc) { + XMEMSET(key_name, 0x5A, WOLFSSL_TICKET_NAME_SZ); + XMEMSET(iv, 0x5A, WOLFSSL_TICKET_IV_SZ); + XMEMSET(mac, 0x5A, WOLFSSL_TICKET_MAC_SZ); + XMEMSET(ticket + inLen, 0, (size_t)(WOLFSSL_TICKET_ENC_SZ - inLen)); + inLen = WOLFSSL_TICKET_ENC_SZ; + } + for (i = 0; i < inLen; i++) + ticket[i] = (byte)(ticket[i] ^ 0xA5); + *outLen = inLen; + return WOLFSSL_TICKET_RET_OK; +} +#endif + +/* A NewSessionTicket larger than the maximum fragment length must be split + * across records (RFC 8446 Section 5.1). The ticket is padded to its largest + * size and the server send limit is lowered below it, so the ticket is always + * fragmented. */ +int test_tls13_new_session_ticket_max_fragment(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MAX_FRAGMENT) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + WOLFSSL_SESSION *sess = NULL; + char readBuf[64]; + const word16 sendLimit = 128; + int off; + int recSz = 0; + int recCnt = 0; + + 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(wolfSSL_CTX_set_TicketEncCb(ctx_s, + test_tls13_nst_max_frag_ticketEncCb), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseMaxFragment(ssl_c, WOLFSSL_MFL_2_8), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Process the ticket sent at the end of the handshake. */ + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_ctx.c_len, 0); + + /* The padded ticket alone exceeds the lowered limit. */ + ExpectIntGT(SESSION_TICKET_LEN, sendLimit); + if (EXPECT_SUCCESS()) { + ssl_s->max_fragment = sendLimit; + } + + ExpectIntEQ(wolfSSL_send_SessionTicket(ssl_s), WOLFSSL_SUCCESS); + /* Each record holds at most the limit plus content type and AEAD tag. */ + for (off = 0; EXPECT_SUCCESS() && off + RECORD_HEADER_SZ <= test_ctx.c_len; + off += RECORD_HEADER_SZ + recSz) { + ExpectIntEQ(test_ctx.c_buff[off], application_data); + recSz = (test_ctx.c_buff[off + 3] << 8) | test_ctx.c_buff[off + 4]; + ExpectIntLE(recSz, sendLimit + 1 + 16); + recCnt++; + } + ExpectIntEQ(off, test_ctx.c_len); + ExpectIntGE(recCnt, 2); + + ExpectIntEQ(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(ssl_c->session->ticketLen, SESSION_TICKET_LEN); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + /* Resume with the reassembled ticket. */ + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + 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(wolfSSL_UseMaxFragment(ssl_c, WOLFSSL_MFL_2_8), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_c), 1); + + wolfSSL_SESSION_free(sess); + 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 50013e8a336..81b2c475934 100644 --- a/tests/api/test_tls13.h +++ b/tests/api/test_tls13.h @@ -183,6 +183,7 @@ int test_tls13_ed448_dev_private_key(void); int test_tls13_ticket_psk_modes(void); int test_tls13_psk_mode_mismatch_falls_back(void); int test_tls13_ticket_psk_modes_uses_policy(void); +int test_tls13_new_session_ticket_max_fragment(void); int test_tls13_send_session_ticket_psk_modes(void); int test_tls13_new_session_ticket_ext_framing(void); int test_tls13_new_session_ticket_keeps_ems(void); @@ -382,6 +383,7 @@ int test_tls13_cks_hrr_reparse(void); TEST_DECL_GROUP("tls13", test_tls13_export_keeps_transport_close), \ TEST_DECL_GROUP("tls13", test_tls12_export_v6_closed_refuses_write), \ TEST_DECL_GROUP("tls13", test_tls13_chain_verify_cb_rpk), \ - TEST_DECL_GROUP("tls13", test_tls13_cks_hrr_reparse) + TEST_DECL_GROUP("tls13", test_tls13_cks_hrr_reparse), \ + TEST_DECL_GROUP("tls13", test_tls13_new_session_ticket_max_fragment) #endif /* WOLFCRYPT_TEST_TLS13_H */ From 5c5247aba7633c405b47b024adc10a770e31d458 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 30 Oct 2026 10:46:08 -0500 Subject: [PATCH 2/2] Fix from review --- doc/dox_comments/header_files/ssl.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 4328cbe7411..bc3ed174b4d 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -6361,10 +6361,10 @@ long wolfSSL_get_default_read_buffer_len(const WOLFSSL* ssl); max_fragment_length the smaller of the two limits applies, where OpenSSL uses the negotiated length instead. A DTLS write larger than the cap fails with DTLS_SIZE_ERROR, like one larger than the MTU, unless - WOLFSSL_NO_DTLS_SIZE_CHECK is defined. In TLS 1.3 only Certificate and - CertificateVerify are split to the cap. Other handshake messages, such as - ClientHello, ServerHello, EncryptedExtensions, CertificateRequest and - NewSessionTicket, are sent in one record each, as they are with a + WOLFSSL_NO_DTLS_SIZE_CHECK is defined. In TLS 1.3 only Certificate, + CertificateVerify and NewSessionTicket are split to the cap. Other + handshake messages, such as ClientHello, ServerHello, EncryptedExtensions + and CertificateRequest, are sent in one record each, as they are with a negotiated max_fragment_length. Application data is always split. \return WOLFSSL_SUCCESS (1) If the cap was set.