From b24306940ffad4dfa9842012531f545b8fbc21bf Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 27 Apr 2026 12:00:45 +0000 Subject: [PATCH 1/2] Speedup DTLS max fragment size calculation for MTU limits Refactor wolfssl_local_GetRecordSize to compute the record size directly from cipher specs instead of calling BuildMessage. Add a unit test that compares the new calculation to BuildMessage's size-only output across every registered cipher suite and supported (D)TLS version. --- src/internal.c | 104 +++++++++++++++++++++---- tests/api/test_tls.c | 179 +++++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls.h | 4 +- wolfssl/internal.h | 8 +- 4 files changed, 273 insertions(+), 22 deletions(-) diff --git a/src/internal.c b/src/internal.c index c10b89d6a6..7f6c4dac9d 100644 --- a/src/internal.c +++ b/src/internal.c @@ -42252,30 +42252,100 @@ int wolfSSL_AsyncPush(WOLFSSL* ssl, WC_ASYNC_DEV* asyncDev) */ int wolfssl_local_GetRecordSize(WOLFSSL *ssl, int payloadSz, int isEncrypted) { - int recordSz; + int sz; + int headerSz; + int digestSz; + int ivSz; +#ifdef WOLFSSL_DTLS_CID + byte cidSz; +#endif +#ifndef WOLFSSL_AEAD_ONLY + int blockSz; + int pad; +#endif if (ssl == NULL) return BAD_FUNC_ARG; - if (isEncrypted) { - recordSz = BuildMessage(ssl, NULL, 0, NULL, payloadSz, application_data, - 0, 1, 0, CUR_ORDER); - /* use a safe upper bound in case of error */ - if (recordSz < 0) { - recordSz = payloadSz + RECORD_HEADER_SZ - + cipherExtraData(ssl) + COMP_EXTRA; - if (ssl->options.dtls) { - recordSz += DTLS_RECORD_EXTRA; - } - } + if (!isEncrypted) { + sz = payloadSz + RECORD_HEADER_SZ; + if (ssl->options.dtls) + sz += DTLS_RECORD_EXTRA; + return sz; } - else { - recordSz = payloadSz + RECORD_HEADER_SZ; - if (ssl->options.dtls) { - recordSz += DTLS_RECORD_EXTRA; + +#ifdef WOLFSSL_TLS13 + if (ssl->options.tls1_3) { + #ifdef WOLFSSL_DTLS13 + if (ssl->options.dtls) + headerSz = Dtls13GetRlHeaderLength(ssl, 1); + else + #endif + headerSz = RECORD_HEADER_SZ; + sz = payloadSz + headerSz + 1 /* inner type */ + + ssl->specs.aead_mac_size; + #ifdef WOLFSSL_DTLS13 + if (ssl->options.dtls && sz < Dtls13MinimumRecordLength(ssl)) + sz = Dtls13MinimumRecordLength(ssl); + #endif + return sz; + } +#endif + + /* TLS 1.2 / TLS 1.1 / DTLS 1.2 path. Mirror BuildMessage's size + * calculation so the result matches exactly. */ + headerSz = RECORD_HEADER_SZ; + sz = payloadSz + RECORD_HEADER_SZ; + + if (ssl->options.dtls) { + sz += DTLS_RECORD_EXTRA; + headerSz += DTLS_RECORD_EXTRA; + #ifdef WOLFSSL_DTLS_CID + cidSz = DtlsGetCidTxSize(ssl); + if (cidSz > 0) { + sz += cidSz; + headerSz += cidSz; + sz++; /* real_type byte appended */ } + #endif + } + + digestSz = (int)ssl->specs.hash_size; +#ifdef HAVE_TRUNCATED_HMAC + if (ssl->truncated_hmac) + digestSz = min(TRUNCATED_HMAC_SZ, digestSz); +#endif + sz += digestSz; + +#ifndef WOLFSSL_AEAD_ONLY + if (ssl->specs.cipher_type == block) { + blockSz = (int)ssl->specs.block_size; + + if (ssl->options.tls1_1) + sz += blockSz; /* explicit IV */ + sz += 1; /* pad-length byte */ + + #if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) + if (ssl->options.startedETMWrite) + pad = blockSz != 0 ? + (sz - headerSz - digestSz) % blockSz : 0; + else + #endif + pad = blockSz != 0 ? (sz - headerSz) % blockSz : 0; + if (pad != 0) + pad = blockSz - pad; + sz += pad; } - return recordSz; + else +#endif /* WOLFSSL_AEAD_ONLY */ + if (ssl->specs.cipher_type == aead) { + ivSz = 0; + if (ssl->specs.bulk_cipher_algorithm != wolfssl_chacha) + ivSz = AESGCM_EXP_IV_SZ; + sz += ivSz + (int)ssl->specs.aead_mac_size - digestSz; + } + + return sz; } #endif diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index aedae4f703..235e6087f1 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -1121,3 +1121,182 @@ int test_tls12_peerauth_failsafe(void) #endif return EXPECT_RESULT(); } + +/* Verify that wolfssl_local_GetRecordSize agrees exactly with + * BuildMessage's size-only output. Iterates every cipher suite registered + * in internal.c via GetCipherNames(), tries each against every supported + * (D)TLS protocol version (with and without DTLS connection ID where + * applicable), and on successful handshake compares the two + * record-size calculations across a range of payload sizes. Logs each + * skipped combination so coverage gaps are visible. Handshake failures + * outside the explicit allow-list fail the test instead of being + * silently skipped. */ +int test_record_size_matches_build_message(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + const int sizes[] = { + 1, 8, 15, 16, 17, 31, 32, 33, 64, 100, 256, 1000, 4096, 16000 + }; + struct { + method_provider client; + method_provider server; + int use_cid; + int is_tls13; + const char* label; + } versions[] = { +#ifndef WOLFSSL_NO_TLS12 + { wolfTLSv1_2_client_method, wolfTLSv1_2_server_method, 0, 0, + "TLSv1.2" }, +#ifdef WOLFSSL_DTLS + { wolfDTLSv1_2_client_method, wolfDTLSv1_2_server_method, 0, 0, + "DTLSv1.2" }, +#ifdef WOLFSSL_DTLS_CID + { wolfDTLSv1_2_client_method, wolfDTLSv1_2_server_method, 1, 0, + "DTLSv1.2+CID" }, +#endif +#endif +#endif +#ifdef WOLFSSL_TLS13 + { wolfTLSv1_3_client_method, wolfTLSv1_3_server_method, 0, 1, + "TLSv1.3" }, +#ifdef WOLFSSL_DTLS13 + { wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method, 0, 1, + "DTLSv1.3" }, +#ifdef WOLFSSL_DTLS_CID + { wolfDTLSv1_3_client_method, wolfDTLSv1_3_server_method, 1, 1, + "DTLSv1.3+CID" }, +#endif +#endif +#endif + }; + const CipherSuiteInfo* allCiphers = GetCipherNames(); + int numCiphers = GetCipherNamesSize(); + int tested = 0; + size_t v, j; + int i; + + fprintf(stderr, "\n"); + for (v = 0; v < XELEM_CNT(versions) && EXPECT_SUCCESS(); v++) { + for (i = 0; i < numCiphers && EXPECT_SUCCESS(); i++) { + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + const char* name = allCiphers[i].name; + int isTls13Cipher = (XSTRSTR(name, "TLS13-") != NULL); + int handshakeRet; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, versions[v].client, versions[v].server), 0); + + /* Skip ciphers that aren't valid for this version/build. */ + if (wolfSSL_set_cipher_list(ssl_c, name) != 1 || + wolfSSL_set_cipher_list(ssl_s, name) != 1) { + fprintf(stderr, + " [SKIP %-12s %-40s] cipher not selectable\n", + versions[v].label, name); + goto next_iter; + } + +#ifdef WOLFSSL_DTLS_CID + if (versions[v].use_cid) { + unsigned char cid_c[] = { 0, 1, 2, 3 }; + unsigned char cid_s[] = { 4, 5, 6, 7, 8, 9 }; + ExpectIntEQ(wolfSSL_dtls_cid_use(ssl_c), 1); + ExpectIntEQ(wolfSSL_dtls_cid_use(ssl_s), 1); + ExpectIntEQ(wolfSSL_dtls_cid_set(ssl_c, cid_s, + (int)sizeof(cid_s)), 1); + ExpectIntEQ(wolfSSL_dtls_cid_set(ssl_s, cid_c, + (int)sizeof(cid_c)), 1); + } +#endif + + handshakeRet = test_memio_do_handshake(ssl_c, ssl_s, 10, NULL); + if (handshakeRet != 0) { + /* Allow-list of ciphers that legitimately can't negotiate + * with the default test_memio configuration. Anything else + * is a real failure. */ + int expected = 0; + const char* reason = NULL; + + if (isTls13Cipher != versions[v].is_tls13) { + expected = 1; + reason = "version mismatch"; + } + else if (XSTRSTR(name, "ECDSA") != NULL) { + expected = 1; + reason = "no ECDSA cert"; + } + else if (XSTRSTR(name, "ECDH-") != NULL) { + expected = 1; + reason = "no static ECDH cert"; + } + else if (XSTRSTR(name, "PSK") != NULL) { + expected = 1; + reason = "no PSK callback"; + } + else if (XSTRSTR(name, "ANON") != NULL || + XSTRSTR(name, "anon") != NULL) { + expected = 1; + reason = "anon not enabled"; + } + else if (XSTRSTR(name, "SRP") != NULL) { + expected = 1; + reason = "no SRP setup"; + } + else if (XSTRSTR(name, "WDM-") != NULL) { + expected = 1; + reason = "multicast not configured"; + } + + if (!expected) { + fprintf(stderr, + " [FAIL %-12s %-40s] unexpected handshake " + "failure (%d)\n", + versions[v].label, name, handshakeRet); + } + else { + fprintf(stderr, + " [SKIP %-12s %-40s] %s\n", + versions[v].label, name, reason); + } + ExpectIntEQ(expected, 1); + goto next_iter; + } + + for (j = 0; j < XELEM_CNT(sizes) && EXPECT_SUCCESS(); j++) { + int payload = sizes[j]; + int recordSz, buildSz; + + recordSz = wolfssl_local_GetRecordSize(ssl_c, payload, 1); + buildSz = BuildMessage(ssl_c, NULL, 0, NULL, payload, + application_data, 0, 1, 0, CUR_ORDER); + + ExpectIntGE(recordSz, 0); + ExpectIntGE(buildSz, 0); + ExpectIntEQ(recordSz, buildSz); + if (recordSz != buildSz) { + fprintf(stderr, + " [MISMATCH %-12s %-40s payload=%d]" + " recordSz=%d buildSz=%d\n", + versions[v].label, name, payload, + recordSz, buildSz); + } + } + tested++; + +next_iter: + wolfSSL_free(ssl_c); wolfSSL_CTX_free(ctx_c); + wolfSSL_free(ssl_s); wolfSSL_CTX_free(ctx_s); + } + } + + /* Sanity: at least one cipher/version combination must have been + * exercised per supported version, otherwise the test is silently a + * no-op. */ + ExpectIntGE(tested, (int)XELEM_CNT(versions)); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_tls.h b/tests/api/test_tls.h index c0f74f2150..2125802295 100644 --- a/tests/api/test_tls.h +++ b/tests/api/test_tls.h @@ -35,6 +35,7 @@ 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); #define TEST_TLS_DECLS \ TEST_DECL_GROUP("tls", test_utils_memio_move_message), \ @@ -49,6 +50,7 @@ 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) #endif /* TESTS_API_TEST_TLS_H */ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 96606ba4f9..a48f57771d 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -6815,7 +6815,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); @@ -7106,8 +7106,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 */ @@ -7187,7 +7187,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); From 6fc100b3e7b6a5b17fc0b85702a775ca4e35f36a Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Tue, 28 Apr 2026 17:19:56 +0200 Subject: [PATCH 2/2] Reject SM cipher suites over DTLS and exercise them in record-size test RFC 8998 registers TLS_SM4_GCM_SM3 / TLS_SM4_CCM_SM3 (and the TLS 1.2 SM4 suites) with DTLS-OK: No and defines no record-number-mask construction for SM4. RFC 9147 Section 4.2.3 requires any non-AES / non-ChaCha20 cipher to define its own record sequence number encryption to be usable with DTLS. Drop SM cipher suites from the negotiable list in InitSuites when DTLS, and reject them defensively in VerifyServerSuite for the case where the user pins a cipher list explicitly. Update test_record_size_matches_build_message to set up an SM2 cert chain and SM2 key share for SM ciphers (so they actually handshake over TLS 1.2 / TLS 1.3) and to skip the DTLS variants with the RFC reason. --- src/internal.c | 30 +++++++++++++++++++++++++----- tests/api/test_tls.c | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/internal.c b/src/internal.c index 7f6c4dac9d..b722443048 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3706,14 +3706,17 @@ void InitSuites(Suites* suites, ProtocolVersion pv, int keySz, word16 haveRSA, #endif #ifdef BUILD_TLS_SM4_GCM_SM3 - if (tls1_3) { + /* RFC 8998 registers TLS_SM4_GCM_SM3 with DTLS-OK: No and provides no + * record-number-mask construction; RFC 9147 Section 4.2.3 forbids using + * non-AES / non-ChaCha20 ciphers over DTLS without one. */ + if (tls1_3 && !dtls) { suites->suites[idx++] = CIPHER_BYTE; suites->suites[idx++] = TLS_SM4_GCM_SM3; } #endif #ifdef BUILD_TLS_SM4_CCM_SM3 - if (tls1_3) { + if (tls1_3 && !dtls) { suites->suites[idx++] = CIPHER_BYTE; suites->suites[idx++] = TLS_SM4_CCM_SM3; } @@ -4647,21 +4650,22 @@ void InitSuites(Suites* suites, ProtocolVersion pv, int keySz, word16 haveRSA, #endif /* BUILD_TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256 */ #ifdef BUILD_TLS_ECDHE_ECDSA_WITH_SM4_CBC_SM3 - if (tls && haveECC) { + /* RFC 8998 registers the SM4 cipher suites with DTLS-OK: No. */ + if (tls && !dtls && haveECC) { suites->suites[idx++] = SM_BYTE; suites->suites[idx++] = TLS_ECDHE_ECDSA_WITH_SM4_CBC_SM3; } #endif #ifdef BUILD_TLS_ECDHE_ECDSA_WITH_SM4_GCM_SM3 - if (tls && haveECC) { + if (tls && !dtls && haveECC) { suites->suites[idx++] = SM_BYTE; suites->suites[idx++] = TLS_ECDHE_ECDSA_WITH_SM4_GCM_SM3; } #endif #ifdef BUILD_TLS_ECDHE_ECDSA_WITH_SM4_CCM_SM3 - if (tls && haveECC) { + if (tls && !dtls && haveECC) { suites->suites[idx++] = SM_BYTE; suites->suites[idx++] = TLS_ECDHE_ECDSA_WITH_SM4_CCM_SM3; } @@ -37283,6 +37287,22 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) first = suites->suites[idx]; second = suites->suites[idx+1]; +#ifdef WOLFSSL_DTLS + /* RFC 8998 registers the SM4 cipher suites with DTLS-OK: No. + * RFC 9147 Section 4.2.3 forbids using non-AES / non-ChaCha20 ciphers + * over DTLS without a defined record-number-mask construction, which + * RFC 8998 does not provide. */ + if (ssl->options.dtls) { + if ((first == CIPHER_BYTE && (second == TLS_SM4_GCM_SM3 || + second == TLS_SM4_CCM_SM3)) || + first == SM_BYTE) { + WOLFSSL_MSG("SM cipher suite not allowed over DTLS " + "(RFC 8998)"); + return 0; + } + } +#endif /* WOLFSSL_DTLS */ + #ifdef WOLFSSL_TLS13 /* When negotiating TLS 1.3, reject non-TLS 1.3 cipher suites */ if (IsAtLeastTLSv1_3(ssl->version) && diff --git a/tests/api/test_tls.c b/tests/api/test_tls.c index 235e6087f1..550bc472ac 100644 --- a/tests/api/test_tls.c +++ b/tests/api/test_tls.c @@ -31,6 +31,9 @@ #include #include #include +#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3) && defined(WOLFSSL_SM4) +#include +#endif int test_utils_memio_move_message(void) @@ -1184,12 +1187,34 @@ int test_record_size_matches_build_message(void) struct test_memio_ctx test_ctx; const char* name = allCiphers[i].name; int isTls13Cipher = (XSTRSTR(name, "TLS13-") != NULL); + int isSmCipher = (XSTRSTR(name, "SM4") != NULL); int handshakeRet; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, - &ssl_s, versions[v].client, versions[v].server), 0); + if (isSmCipher) { +#if defined(WOLFSSL_SM2) && defined(WOLFSSL_SM3) && defined(WOLFSSL_SM4) + ExpectIntEQ(test_memio_setup_ex(&test_ctx, &ctx_c, &ctx_s, + &ssl_c, &ssl_s, versions[v].client, versions[v].server, + (byte*)ca_sm2_der, (int)sizeof_ca_sm2_der, + (byte*)server_sm2_der, (int)sizeof_server_sm2_der, + (byte*)server_sm2_priv_der, + (int)sizeof_server_sm2_priv_der), 0); + if (versions[v].is_tls13) { + ExpectIntEQ(wolfSSL_UseKeyShare(ssl_c, + WOLFSSL_ECC_SM2P256V1), 1); + } +#else + fprintf(stderr, + " [SKIP %-12s %-40s] SM build not enabled\n", + versions[v].label, name); + goto next_iter; +#endif + } + else { + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, versions[v].client, versions[v].server), 0); + } /* Skip ciphers that aren't valid for this version/build. */ if (wolfSSL_set_cipher_list(ssl_c, name) != 1 || @@ -1200,6 +1225,14 @@ int test_record_size_matches_build_message(void) goto next_iter; } + if (isSmCipher && + XSTRNCMP(versions[v].label, "DTLS", 4) == 0) { + fprintf(stderr, + " [SKIP %-12s %-40s] RFC 8998 forbids SM suites over DTLS\n", + versions[v].label, name); + goto next_iter; + } + #ifdef WOLFSSL_DTLS_CID if (versions[v].use_cid) { unsigned char cid_c[] = { 0, 1, 2, 3 }; @@ -1225,7 +1258,7 @@ int test_record_size_matches_build_message(void) expected = 1; reason = "version mismatch"; } - else if (XSTRSTR(name, "ECDSA") != NULL) { + else if (XSTRSTR(name, "ECDSA") != NULL && !isSmCipher) { expected = 1; reason = "no ECDSA cert"; }