From 9e052cd1697bac07dae2028d3fd9f59293c0bc0b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 13 May 2026 11:26:38 +0000 Subject: [PATCH] Cache AEAD record overhead on WOLFSSL wolfssl_local_GetRecordSize() runs BuildMessage(sizeOnly=1) on every wolfSSL_write hot path. For AEAD ciphers the overhead is constant per connection framing state, so cache (recordSz - payloadSz) on WOLFSSL and invalidate in SetKeysSide(), at cidInfo->tx assignment, and in wolfSSL_clear(). BuildMessage stays the single source of truth. Add test_record_size_matches_build_message (cross-checks every built cipher over TLS/DTLS +/- CID against BuildMessage) and test_record_size_cache_invalidated_on_renegotiation. --- src/dtls.c | 8 ++ src/internal.c | 29 +++++++ src/keys.c | 6 ++ src/ssl.c | 1 + tests/api/test_tls.c | 197 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 7 +- wolfssl/internal.h | 12 ++- 7 files changed, 255 insertions(+), 5 deletions(-) diff --git a/src/dtls.c b/src/dtls.c index b79db10330e..e26e1fd1c3c 100644 --- a/src/dtls.c +++ b/src/dtls.c @@ -1311,6 +1311,14 @@ int TLSX_ConnectionID_Parse(WOLFSSL* ssl, const byte* input, word16 length, XMEMCPY(id->id, input + OPAQUE8_LEN, cidSz); id->length = cidSz; info->tx = id; + /* Invalidate the cached AEAD record overhead because the TX CID + * changes record framing. Today this only fires during the initial + * extension exchange (before the cache can be populated). When + * mid-connection CID change is added (DTLS 1.3), add a regression + * test that primes the cache, changes the CID, and re-asserts that + * wolfssl_local_GetRecordSize() agrees with BuildMessage(sizeOnly=1) + * after the change. */ + ssl->recordSzOverhead = 0; } info->negotiated = 1; diff --git a/src/internal.c b/src/internal.c index f19d8fa10d6..c909259875a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -42646,6 +42646,27 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) return BAD_FUNC_ARG; if (isEncrypted) { + /* AEAD overhead is constant per cache key (cipher, version, CID, DTLS + * 1.3 epoch); use the cached value when available. DTLS 1.3 pads + * records up to Dtls13MinimumRecordLength() (RFC 9147 5.5), so: + * - on read: only return the cached overhead when the resulting + * record would not be padded; + * - on populate: only store the overhead when BuildMessage returned + * a record strictly above the minimum, which guarantees no + * padding was applied. */ +#ifdef WOLFSSL_DTLS13 + int isDtls13 = ssl->options.dtls && ssl->options.tls1_3; +#endif + + if (ssl->specs.cipher_type == aead && ssl->recordSzOverhead != 0 +#ifdef WOLFSSL_DTLS13 + && (!isDtls13 || payloadSz + (int)ssl->recordSzOverhead + >= Dtls13MinimumRecordLength(ssl)) +#endif + ) { + return payloadSz + (int)ssl->recordSzOverhead; + } + recordSz = BuildMessage(ssl, NULL, 0, NULL, payloadSz, application_data, 0, 1, 0, CUR_ORDER); /* use a safe upper bound in case of error */ @@ -42656,6 +42677,14 @@ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) recordSz += DTLS_RECORD_EXTRA; } } + else if (ssl->specs.cipher_type == aead && recordSz > payloadSz +#ifdef WOLFSSL_DTLS13 + && (!isDtls13 || recordSz > Dtls13MinimumRecordLength(ssl)) +#endif + ) { + /* Populate cache only on success; never from the fallback. */ + ssl->recordSzOverhead = (word32)(recordSz - payloadSz); + } } else { recordSz = payloadSz + RECORD_HEADER_SZ; diff --git a/src/keys.c b/src/keys.c index 74b094fc59a..002c0fb31e4 100644 --- a/src/keys.c +++ b/src/keys.c @@ -3499,6 +3499,12 @@ int SetKeysSide(WOLFSSL* ssl, enum encrypt_side side) (void)copy; + /* Cipher activation invalidates the cached AEAD record overhead. Covers + * TLS 1.2 / TLS 1.3 handshake completion, secure renegotiation, early + * data flips, and DTLS 1.3 epoch transitions (Dtls13SetEpochKeys() calls + * SetKeysSide() at the bottom). */ + ssl->recordSzOverhead = 0; + #ifdef HAVE_SECURE_RENEGOTIATION if (ssl->secure_renegotiation && ssl->secure_renegotiation->cache_status != SCR_CACHE_NULL) { diff --git a/src/ssl.c b/src/ssl.c index 9b694806e19..bbc1435403e 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -10135,6 +10135,7 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, ssl->options.acceptState = ACCEPT_BEGIN; ssl->options.handShakeState = NULL_STATE; ssl->options.handShakeDone = 0; + ssl->recordSzOverhead = 0; ssl->options.processReply = 0; /* doProcessInit */ ssl->options.havePeerVerify = 0; ssl->options.havePeerCert = 0; diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index aedae4f7031..d2fd4833e79 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -1121,3 +1121,200 @@ int test_tls12_peerauth_failsafe(void) #endif return EXPECT_RESULT(); } + +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) +/* Cipher-name substrings that need extra setup (PSK callback, ECDSA cert, + * SRP, etc.) which the default test_memio_setup() doesn't provide. */ +static int record_size_skip_cipher(const char *name) +{ + /* "ECDH-" matches static-ECDH ciphers ("ECDH-RSA-*", "ECDH-ECDSA-*") + * and not ECDHE-* because of the trailing '-'. */ + static const char* const deny[] = { + "PSK", "SRP", "ANON", "NULL", "ECDSA", "ECDH-", "SM" + }; + size_t i; + for (i = 0; i < XELEM_CNT(deny); i++) { + if (XSTRSTR(name, deny[i]) != NULL) + return 1; + } + return 0; +} + +/* Cross-check wolfssl_local_GetRecordSize() against BuildMessage(sizeOnly=1) + * with the cache cold, then call it a second time and assert both calls + * return the same size — that exercises the cached path for AEAD ciphers + * without duplicating the BuildMessage arithmetic. */ +static int record_size_check_ssl(WOLFSSL *ssl) +{ + EXPECT_DECLS; + static const int payloads[] = { 1, 16, 256, 1300, 4096 }; + size_t k; + + for (k = 0; k < XELEM_CNT(payloads); k++) { + int payloadSz = payloads[k]; + int expectedSz = BuildMessage(ssl, NULL, 0, NULL, payloadSz, + application_data, 0, 1, 0, CUR_ORDER); + int firstSz, secondSz; + + ssl->recordSzOverhead = 0; + firstSz = wolfssl_local_GetRecordSize(ssl, payloadSz, 1); + secondSz = wolfssl_local_GetRecordSize(ssl, payloadSz, 1); + ExpectIntEQ(firstSz, expectedSz); + ExpectIntEQ(secondSz, expectedSz); + } + return EXPECT_RESULT(); +} + +/* Returns 1 if `suite` is selectable for the given client/server method + * pair, 0 otherwise. wolfSSL rejects some ciphers for DTLS at + * set_cipher_list time (e.g. RFC 7465 forbids RC4 in DTLS); skip those + * silently rather than failing the cross-check. */ +static int record_size_cipher_selectable(method_provider client_method, + method_provider server_method, const char *suite) +{ + WOLFSSL_CTX *ctx_c = wolfSSL_CTX_new(client_method()); + WOLFSSL_CTX *ctx_s = wolfSSL_CTX_new(server_method()); + int ok = (ctx_c != NULL && ctx_s != NULL && + wolfSSL_CTX_set_cipher_list(ctx_c, suite) == WOLFSSL_SUCCESS && + wolfSSL_CTX_set_cipher_list(ctx_s, suite) == WOLFSSL_SUCCESS); + if (ctx_c) wolfSSL_CTX_free(ctx_c); + if (ctx_s) wolfSSL_CTX_free(ctx_s); + return ok; +} + +/* Run the cross-check on a memio pair using the given (de)multiplexing + * methods and cipher suite. Optionally enable DTLS-CID with peer CIDs of + * different sizes so the test covers CID-extended record framing. */ +static int record_size_run_pair(method_provider client_method, + method_provider server_method, const char *suite, int useCid) +{ + EXPECT_DECLS; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + (void)useCid; + if (!record_size_cipher_selectable(client_method, server_method, suite)) + return TEST_SUCCESS; /* not valid for this protocol -- skip */ + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_ciphers = test_ctx.s_ciphers = suite; + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + client_method, server_method), 0); +#ifdef WOLFSSL_DTLS_CID + if (useCid) { + /* Different sizes on each side to exercise asymmetric framing. */ + static unsigned char client_cid[] = { 1, 2, 3, 4, 5, 6 }; + static unsigned char server_cid[] = { 7, 8, 9 }; + ExpectIntEQ(wolfSSL_dtls_cid_use(ssl_c), 1); + ExpectIntEQ(wolfSSL_dtls_cid_set(ssl_c, server_cid, + sizeof(server_cid)), 1); + ExpectIntEQ(wolfSSL_dtls_cid_use(ssl_s), 1); + ExpectIntEQ(wolfSSL_dtls_cid_set(ssl_s, client_cid, + sizeof(client_cid)), 1); + } +#endif + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 30, NULL), 0); + ExpectIntEQ(record_size_check_ssl(ssl_c), TEST_SUCCESS); + ExpectIntEQ(record_size_check_ssl(ssl_s), TEST_SUCCESS); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + return EXPECT_RESULT(); +} +#endif /* HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES */ + +int test_record_size_matches_build_message(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + const CipherSuiteInfo *suites = GetCipherNames(); + int n = GetCipherNamesSize(); + int i; + + for (i = 0; i < n; i++) { + const char *name = suites[i].name; + /* Names prefixed "TLS13-" are TLS 1.3 suites regardless of + * cipherSuite0, which may be either TLS13_BYTE or ECC_BYTE (for + * the integrity-only TLS_SHA*_SHA* suites). */ + int isTls13 = (XSTRNCMP(name, "TLS13-", 6) == 0); + if (record_size_skip_cipher(name)) + continue; + + if (isTls13) { +#ifdef WOLFSSL_TLS13 + ExpectIntEQ(record_size_run_pair(wolfTLSv1_3_client_method, + wolfTLSv1_3_server_method, name, 0), TEST_SUCCESS); +#endif +#ifdef WOLFSSL_DTLS13 + ExpectIntEQ(record_size_run_pair(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, name, 0), TEST_SUCCESS); +#if defined(WOLFSSL_DTLS_CID) + ExpectIntEQ(record_size_run_pair(wolfDTLSv1_3_client_method, + wolfDTLSv1_3_server_method, name, 1), TEST_SUCCESS); +#endif +#endif + } + else { +#ifndef WOLFSSL_NO_TLS12 + ExpectIntEQ(record_size_run_pair(wolfTLSv1_2_client_method, + wolfTLSv1_2_server_method, name, 0), TEST_SUCCESS); +#endif +#if defined(WOLFSSL_DTLS) && !defined(WOLFSSL_NO_TLS12) + ExpectIntEQ(record_size_run_pair(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method, name, 0), TEST_SUCCESS); +#if defined(WOLFSSL_DTLS_CID) + ExpectIntEQ(record_size_run_pair(wolfDTLSv1_2_client_method, + wolfDTLSv1_2_server_method, name, 1), TEST_SUCCESS); +#endif +#endif + } + } +#endif /* HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES */ + return EXPECT_RESULT(); +} + +int test_record_size_cache_invalidated_on_renegotiation(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_SECURE_RENEGOTIATION) && !defined(WOLFSSL_NO_TLS12) && \ + defined(BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + byte readBuf[16]; + int sz; + + 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(wolfSSL_UseSecureRenegotiation(ssl_c), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSecureRenegotiation(ssl_s), WOLFSSL_SUCCESS); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + sz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); + ExpectIntEQ(sz, BuildMessage(ssl_c, NULL, 0, NULL, 256, + application_data, 0, 1, 0, CUR_ORDER)); + ExpectIntNE(ssl_c->recordSzOverhead, 0); + + ExpectIntEQ(wolfSSL_Rehandshake(ssl_c), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(wolfSSL_read(ssl_s, readBuf, sizeof(readBuf)), -1); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* SetKeysSide() during renegotiation must have cleared the cache. */ + sz = wolfssl_local_GetRecordSize(ssl_c, 256, 1); + ExpectIntEQ(sz, BuildMessage(ssl_c, NULL, 0, NULL, 256, + application_data, 0, 1, 0, CUR_ORDER)); + + 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_tls.h b/tests/api/test_tls.h index c0f74f21505..8d6285540a8 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -35,6 +35,8 @@ int test_tls12_etm_failed_resumption(void); int test_tls_set_curves_list_ecc_fallback(void); int test_tls12_corrupted_finished(void); int test_tls12_peerauth_failsafe(void); +int test_record_size_matches_build_message(void); +int test_record_size_cache_invalidated_on_renegotiation(void); #define TEST_TLS_DECLS \ TEST_DECL_GROUP("tls", test_utils_memio_move_message), \ @@ -49,6 +51,9 @@ int test_tls12_peerauth_failsafe(void); TEST_DECL_GROUP("tls", test_tls12_etm_failed_resumption), \ TEST_DECL_GROUP("tls", test_tls_set_curves_list_ecc_fallback), \ TEST_DECL_GROUP("tls", test_tls12_corrupted_finished), \ - TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe) + TEST_DECL_GROUP("tls", test_tls12_peerauth_failsafe), \ + TEST_DECL_GROUP("tls", test_record_size_matches_build_message), \ + TEST_DECL_GROUP("tls", \ + test_record_size_cache_invalidated_on_renegotiation) #endif /* TESTS_API_TEST_TLS_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 5daab5620f1..ebd0d2623e4 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -6602,6 +6602,10 @@ struct WOLFSSL { #endif #endif #endif + /* Cached BuildMessage(sizeOnly) overhead (recordSz - payloadSz) for AEAD + * ciphers; 0 means uncached and is never a valid AEAD overhead. EtM does + * not apply to AEAD. */ + word32 recordSzOverhead; }; #if defined(WOLFSSL_SYS_CRYPTO_POLICY) @@ -6876,7 +6880,7 @@ WOLFSSL_LOCAL int VerifyClientSuite(word16 havePSK, byte cipherSuite0, byte cipherSuite); WOLFSSL_LOCAL int SetTicket(WOLFSSL* ssl, const byte* ticket, word32 length); -WOLFSSL_LOCAL int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, +WOLFSSL_TEST_VIS int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted); WOLFSSL_LOCAL int wolfssl_local_GetMaxPlaintextSize(WOLFSSL *ssl); WOLFSSL_LOCAL int wolfSSL_GetMaxFragSize(WOLFSSL* ssl); @@ -7167,8 +7171,8 @@ typedef struct CipherSuiteInfo { byte flags; } CipherSuiteInfo; -WOLFSSL_LOCAL const CipherSuiteInfo* GetCipherNames(void); -WOLFSSL_LOCAL int GetCipherNamesSize(void); +WOLFSSL_TEST_VIS const CipherSuiteInfo* GetCipherNames(void); +WOLFSSL_TEST_VIS int GetCipherNamesSize(void); WOLFSSL_LOCAL const char* GetCipherNameInternal(byte cipherSuite0, byte cipherSuite); #if defined(OPENSSL_ALL) || defined(WOLFSSL_QT) /* used in wolfSSL_sk_CIPHER_description */ @@ -7248,7 +7252,7 @@ WOLFSSL_LOCAL int InitHandshakeHashesAndCopy(WOLFSSL* ssl, HS_Hashes* source, #ifndef WOLFSSL_NO_TLS12 WOLFSSL_LOCAL void FreeBuildMsgArgs(WOLFSSL* ssl, BuildMsgArgs* args); #endif -WOLFSSL_LOCAL int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, +WOLFSSL_TEST_VIS int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input, int inSz, int type, int hashOutput, int sizeOnly, int asyncOkay, int epochOrder);