From c92861ea1ae15f257255a7f457b0ce432dc7200a Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:43:05 +0200 Subject: [PATCH 01/12] tls: fix TLSX_ALPN_GetSize word16 overflow (F-2128) Match the TLSX_SNI_GetSize pattern: use a word32 accumulator and return 0 if the aggregate size exceeds WOLFSSL_MAX_16BIT, so a large number of ALPN entries can no longer silently wrap the length computation. --- src/tls.c | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/tls.c b/src/tls.c index f8ba67db70..d44b6fe194 100644 --- a/src/tls.c +++ b/src/tls.c @@ -1809,16 +1809,20 @@ static void TLSX_ALPN_FreeAll(ALPN *list, void* heap) static word16 TLSX_ALPN_GetSize(ALPN *list) { ALPN* alpn; - word16 length = OPAQUE16_LEN; /* list length */ + word32 length = OPAQUE16_LEN; /* list length */ while ((alpn = list)) { list = alpn->next; length++; /* protocol name length is on one byte */ - length += (word16)XSTRLEN(alpn->protocol_name); + length += (word32)XSTRLEN(alpn->protocol_name); + + if (length > WOLFSSL_MAX_16BIT) { + return 0; + } } - return length; + return (word16)length; } /** Writes the ALPN objects of a list in a buffer. */ @@ -14926,9 +14930,16 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, isRequest); break; - case TLSX_APPLICATION_LAYER_PROTOCOL: - length += ALPN_GET_SIZE((ALPN*)extension->data); + case TLSX_APPLICATION_LAYER_PROTOCOL: { + word16 alpnSz = ALPN_GET_SIZE((ALPN*)extension->data); + /* 0 on non-empty list means 16-bit overflow. */ + if (alpnSz == 0 && extension->data != NULL) { + ret = LENGTH_ERROR; + break; + } + length += alpnSz; break; + } #if !defined(NO_CERTS) && !defined(WOLFSSL_NO_SIGALG) case TLSX_SIGNATURE_ALGORITHMS: length += SA_GET_SIZE(extension->data); From 246cc90300c89b150e7d7320790ad5162035f426 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:43:26 +0200 Subject: [PATCH 02/12] tls: fix TLSX_TCA_GetSize word16 overflow (F-2131) Mirror the TLSX_SNI_GetSize pattern: accumulate into a word32 and return 0 when the aggregate size exceeds WOLFSSL_MAX_16BIT so large idSz values or many TCA entries no longer silently wrap to a small value that undersizes the TLSX_TCA_Write output buffer. --- src/tls.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/tls.c b/src/tls.c index d44b6fe194..d95b7bdeba 100644 --- a/src/tls.c +++ b/src/tls.c @@ -2945,7 +2945,7 @@ static void TLSX_TCA_FreeAll(TCA* list, void* heap) static word16 TLSX_TCA_GetSize(TCA* list) { TCA* tca; - word16 length = OPAQUE16_LEN; /* list length */ + word32 length = OPAQUE16_LEN; /* list length */ while ((tca = list)) { list = tca->next; @@ -2963,9 +2963,13 @@ static word16 TLSX_TCA_GetSize(TCA* list) length += OPAQUE16_LEN + tca->idSz; break; } + + if (length > WOLFSSL_MAX_16BIT) { + return 0; + } } - return length; + return (word16)length; } /** Writes the TCA objects of a list in a buffer. */ @@ -14888,8 +14892,15 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, case TLSX_TRUSTED_CA_KEYS: /* TCA only sends the list on the request. */ - if (isRequest) - length += TCA_GET_SIZE((TCA*)extension->data); + if (isRequest) { + word16 tcaSz = TCA_GET_SIZE((TCA*)extension->data); + /* 0 on non-empty list means 16-bit overflow. */ + if (tcaSz == 0 && extension->data != NULL) { + ret = LENGTH_ERROR; + break; + } + length += tcaSz; + } break; case TLSX_MAX_FRAGMENT_LENGTH: From 6ca048dc0a52c33c6b9a5109a3bfec15cda3626a Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:44:04 +0200 Subject: [PATCH 03/12] tls: fix TLSX_PreSharedKey_GetSize word16 overflow (F-2925) Both TLSX_PreSharedKey_GetSize and TLSX_PreSharedKey_GetSizeBinders accumulate per-identity bytes into a word16. With enough PSK entries (or large binderLen/identityLen values) the accumulator wraps silently and the caller allocates an undersized extension buffer, which TLSX_PreSharedKey_Write then overflows. Switch both accumulators to word32 and return LENGTH_ERROR when the total would exceed the 16-bit wire length field. --- src/tls.c | 63 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/tls.c b/src/tls.c index d95b7bdeba..d8efb8c9c8 100644 --- a/src/tls.c +++ b/src/tls.c @@ -11911,14 +11911,22 @@ static int TLSX_PreSharedKey_GetSize(PreSharedKey* list, byte msgType, { if (msgType == client_hello) { /* Length of identities + Length of binders. */ - word16 len = OPAQUE16_LEN + OPAQUE16_LEN; + word32 len = OPAQUE16_LEN + OPAQUE16_LEN; while (list != NULL) { /* Each entry has: identity, ticket age and binder. */ len += OPAQUE16_LEN + list->identityLen + OPAQUE32_LEN + - OPAQUE8_LEN + (word16)list->binderLen; + OPAQUE8_LEN + (word32)list->binderLen; + if (len > WOLFSSL_MAX_16BIT) { + WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); + return LENGTH_ERROR; + } list = list->next; } - *pSz += len; + if ((word32)*pSz + len > WOLFSSL_MAX_16BIT) { + WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); + return LENGTH_ERROR; + } + *pSz += (word16)len; return 0; } @@ -11941,7 +11949,7 @@ static int TLSX_PreSharedKey_GetSize(PreSharedKey* list, byte msgType, int TLSX_PreSharedKey_GetSizeBinders(PreSharedKey* list, byte msgType, word16* pSz) { - word16 len; + word32 len; if (msgType != client_hello) { WOLFSSL_ERROR_VERBOSE(SANITY_MSG_E); @@ -11951,11 +11959,15 @@ int TLSX_PreSharedKey_GetSizeBinders(PreSharedKey* list, byte msgType, /* Length of all binders. */ len = OPAQUE16_LEN; while (list != NULL) { - len += OPAQUE8_LEN + (word16)list->binderLen; + len += OPAQUE8_LEN + (word32)list->binderLen; + if (len > WOLFSSL_MAX_16BIT) { + WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); + return LENGTH_ERROR; + } list = list->next; } - *pSz = len; + *pSz = (word16)len; return 0; } @@ -14858,7 +14870,10 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, { int ret = 0; TLSX* extension; - word16 length = 0; + /* Accumulate in word32 so per-extension additions can't silently wrap + * a word16 total - the final 16-bit bound is enforced once at the end. */ + word32 length = 0; + word16 hsz; byte isRequest = (msgType == client_hello || msgType == certificate_request); @@ -14958,19 +14973,25 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, #endif #if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) case TLSX_ENCRYPT_THEN_MAC: - ret = ETM_GET_SIZE(msgType, &length); + hsz = 0; + ret = ETM_GET_SIZE(msgType, &hsz); + length += hsz; break; #endif /* HAVE_ENCRYPT_THEN_MAC */ #if defined(WOLFSSL_TLS13) || !defined(WOLFSSL_NO_TLS12) || !defined(NO_OLD_TLS) #if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) case TLSX_PRE_SHARED_KEY: + hsz = 0; ret = PSK_GET_SIZE((PreSharedKey*)extension->data, msgType, - &length); + &hsz); + length += hsz; break; #ifdef WOLFSSL_TLS13 case TLSX_PSK_KEY_EXCHANGE_MODES: - ret = PKM_GET_SIZE((byte)extension->val, msgType, &length); + hsz = 0; + ret = PKM_GET_SIZE((byte)extension->val, msgType, &hsz); + length += hsz; break; #ifdef WOLFSSL_CERT_WITH_EXTERN_PSK case TLSX_CERT_WITH_EXTERN_PSK: @@ -14986,22 +15007,30 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, #ifdef WOLFSSL_TLS13 case TLSX_SUPPORTED_VERSIONS: - ret = SV_GET_SIZE(extension->data, msgType, &length); + hsz = 0; + ret = SV_GET_SIZE(extension->data, msgType, &hsz); + length += hsz; break; case TLSX_COOKIE: - ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &length); + hsz = 0; + ret = CKE_GET_SIZE((Cookie*)extension->data, msgType, &hsz); + length += hsz; break; #ifdef WOLFSSL_EARLY_DATA case TLSX_EARLY_DATA: - ret = EDI_GET_SIZE(msgType, &length); + hsz = 0; + ret = EDI_GET_SIZE(msgType, &hsz); + length += hsz; break; #endif #ifdef WOLFSSL_POST_HANDSHAKE_AUTH case TLSX_POST_HANDSHAKE_AUTH: - ret = PHA_GET_SIZE(msgType, &length); + hsz = 0; + ret = PHA_GET_SIZE(msgType, &hsz); + length += hsz; break; #endif @@ -15059,7 +15088,11 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, TURN_ON(semaphore, TLSX_ToSemaphore((word16)extension->type)); } - *pLength += length; + if (ret == 0 && (word32)*pLength + length > WOLFSSL_MAX_16BIT) { + WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); + return LENGTH_ERROR; + } + *pLength += (word16)length; return ret; } From 0cf031b8f87b808d7ee02fc810edc513eded4e6f Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:44:24 +0200 Subject: [PATCH 04/12] tls: fix TLSX_CA_Names_GetSize word16 overflow (F-2927) The CA Names extension size accumulator was a word16. With enough CA entries (or large DER-encoded names) the running total can wrap silently, leaving TLSX_CA_Names_Write to overflow an undersized extension buffer. Match TLSX_SNI_GetSize: use a word32 accumulator and return 0 when the total exceeds WOLFSSL_MAX_16BIT. --- src/tls.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/tls.c b/src/tls.c index d8efb8c9c8..506237bf1a 100644 --- a/src/tls.c +++ b/src/tls.c @@ -7576,7 +7576,7 @@ static word16 TLSX_CA_Names_GetSize(void* data) { WOLFSSL* ssl = (WOLFSSL*)data; WOLF_STACK_OF(WOLFSSL_X509_NAME)* names; - word16 size = 0; + word32 size = 0; /* Length of names */ size += OPAQUE16_LEN; @@ -7586,11 +7586,14 @@ static word16 TLSX_CA_Names_GetSize(void* data) if (name != NULL) { /* 16-bit length | SEQ | Len | DER of name */ - size += (word16)(OPAQUE16_LEN + SetSequence(name->rawLen, seq) + + size += (word32)(OPAQUE16_LEN + SetSequence(name->rawLen, seq) + name->rawLen); + if (size > WOLFSSL_MAX_16BIT) { + return 0; + } } } - return size; + return (word16)size; } static word16 TLSX_CA_Names_Write(void* data, byte* output) @@ -15041,9 +15044,16 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, #endif #if !defined(NO_CERTS) && !defined(WOLFSSL_NO_CA_NAMES) - case TLSX_CERTIFICATE_AUTHORITIES: - length += CAN_GET_SIZE(extension->data); + case TLSX_CERTIFICATE_AUTHORITIES: { + word16 canSz = CAN_GET_SIZE(extension->data); + /* 0 on non-empty list means 16-bit overflow. */ + if (canSz == 0 && extension->data != NULL) { + ret = LENGTH_ERROR; + break; + } + length += canSz; break; + } #endif #endif #ifdef WOLFSSL_SRTP From 37fda898c1f616de4f7d290d418e77d2ce3d146b Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:48:54 +0200 Subject: [PATCH 05/12] tests: add EMS resumption downgrade negative test (F-2915) Covers the HandleResumeHistory check that RFC 7627 Section 5.3 requires: if the original session used Extended Master Secret, the server MUST abort when a resumption ClientHello is received without EMS. The new memio test performs a TLS 1.2 handshake with EMS, saves the session, disables EMS on a fresh client, resumes with the saved session, and asserts the server returns EXT_MASTER_SECRET_NEEDED_E. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 50 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 52 insertions(+) diff --git a/tests/api.c b/tests/api.c index 7cde2a6bdc..10d0a74eab 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36298,6 +36298,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_select_next_proto), #endif TEST_DECL(test_tls_ems_downgrade), + TEST_DECL(test_tls_ems_resumption_downgrade), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index ad6053f727..4dd736f5eb 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -103,6 +103,56 @@ int test_tls_ems_downgrade(void) } +/* F-2915: resumption of an EMS session without EMS must abort with + * EXT_MASTER_SECRET_NEEDED_E (RFC 7627 Section 5.3). */ +int test_tls_ems_resumption_downgrade(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && defined(HAVE_EXTENDED_MASTER) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_SESSION_CACHE) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *session = NULL; + + 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); + + ExpectNotNull(session = wolfSSL_get1_session(ssl_c)); + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + test_memio_clear_buffer(&test_ctx, 0); + test_memio_clear_buffer(&test_ctx, 1); + + 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_set_session(ssl_c, session), WOLFSSL_SUCCESS); + /* Drop EMS from the resumption ClientHello to simulate a downgrade. */ + ExpectIntEQ(wolfSSL_DisableExtendedMasterSecret(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, 0), + WC_NO_ERR_TRACE(EXT_MASTER_SECRET_NEEDED_E)); + + wolfSSL_SESSION_free(session); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index ec7a5223be..8d9d8fcb9f 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -23,6 +23,7 @@ #define TESTS_API_TEST_TLS_EXT_H int test_tls_ems_downgrade(void); +int test_tls_ems_resumption_downgrade(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From b59d5c3fad2cab7eb07a0677595c31ee69787847 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:52:02 +0200 Subject: [PATCH 06/12] tests: add ChaCha20-Poly1305 AEAD tag negative test (F-2921) Cover the Poly1305 ConstantCompare tag check in ChachaAEADDecrypt that no existing test was hitting (VERIFY_MAC_ERROR never expected in the suite). A memio-based TLS 1.2 handshake over ECDHE-RSA-CHACHA20-POLY1305 completes, the server's IORecv is then replaced with a wrapper that flips the final byte of the next record body so the forged Poly1305 tag no longer matches. The server's wolfSSL_read must surface VERIFY_MAC_ERROR. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 66 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 68 insertions(+) diff --git a/tests/api.c b/tests/api.c index 10d0a74eab..fbfde54cd8 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36299,6 +36299,7 @@ TEST_CASE testCases[] = { #endif TEST_DECL(test_tls_ems_downgrade), TEST_DECL(test_tls_ems_resumption_downgrade), + TEST_DECL(test_tls12_chacha20_poly1305_bad_tag), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 4dd736f5eb..6643b38565 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -153,6 +153,72 @@ int test_tls_ems_resumption_downgrade(void) } +#if !defined(WOLFSSL_NO_TLS12) && \ + defined(BUILD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) +static int test_chacha_bad_tag_trigger = 0; + +static int test_chacha_bad_tag_io_recv(WOLFSSL* ssl, char* buf, int sz, + void* ctx) +{ + int ret = test_memio_read_cb(ssl, buf, sz, ctx); + /* Tamper with a byte from the encrypted record payload on the first + * read that spans past the 5-byte TLS record header, so the Poly1305 + * authentication check no longer matches. */ + if (test_chacha_bad_tag_trigger && ret > 5) { + buf[ret - 1] ^= 0xFF; + test_chacha_bad_tag_trigger = 0; + } + return ret; +} +#endif + +/* F-2921: TLS 1.2 ChaCha20-Poly1305 must surface VERIFY_MAC_ERROR when + * the Poly1305 tag is corrupted. */ +int test_tls12_chacha20_poly1305_bad_tag(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && \ + defined(BUILD_TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + const char msg[] = "tamper me"; + char recvBuf[32]; + int ret; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_ciphers = test_ctx.s_ciphers = + "ECDHE-RSA-CHACHA20-POLY1305"; + + 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); + + wolfSSL_SSLSetIORecv(ssl_s, test_chacha_bad_tag_io_recv); + + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)XSTRLEN(msg)), + (int)XSTRLEN(msg)); + + test_chacha_bad_tag_trigger = 1; + ret = wolfSSL_read(ssl_s, recvBuf, sizeof(recvBuf)); + ExpectIntLE(ret, 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, ret), + WC_NO_ERR_TRACE(VERIFY_MAC_ERROR)); + + test_chacha_bad_tag_trigger = 0; + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index 8d9d8fcb9f..fb8a23d1a2 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -24,6 +24,7 @@ int test_tls_ems_downgrade(void); int test_tls_ems_resumption_downgrade(void); +int test_tls12_chacha20_poly1305_bad_tag(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From f2532ca0092752a5936e6e03813a4a3fc40f98d0 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:53:38 +0200 Subject: [PATCH 07/12] tests: add TLS 1.3 null cipher HMAC negative test (F-2916) Tls13IntegrityOnly_Decrypt was completely untouched by existing tests, so any mutation of its ConstantCompare would pass CI. Add a memio TLS 1.3 handshake over TLS13-SHA256-SHA256 (integrity-only NULL cipher), then corrupt the final byte of the next record body via an IORecv wrapper and assert the server surfaces DECRYPT_ERROR. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 65 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 67 insertions(+) diff --git a/tests/api.c b/tests/api.c index fbfde54cd8..8624423b4e 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36300,6 +36300,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls_ems_downgrade), TEST_DECL(test_tls_ems_resumption_downgrade), TEST_DECL(test_tls12_chacha20_poly1305_bad_tag), + TEST_DECL(test_tls13_null_cipher_bad_hmac), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 6643b38565..976a3d9490 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -219,6 +219,71 @@ int test_tls12_chacha20_poly1305_bad_tag(void) } +#if defined(WOLFSSL_TLS13) && defined(HAVE_NULL_CIPHER) && \ + defined(BUILD_TLS_SHA256_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) +static int test_tls13_null_bad_hmac_trigger = 0; + +static int test_tls13_null_bad_hmac_io_recv(WOLFSSL* ssl, char* buf, int sz, + void* ctx) +{ + int ret = test_memio_read_cb(ssl, buf, sz, ctx); + /* Tamper with a byte from the encrypted record payload on the first + * read that spans past the 5-byte TLS record header, so the HMAC tag + * check in Tls13IntegrityOnly_Decrypt no longer matches. */ + if (test_tls13_null_bad_hmac_trigger && ret > 5) { + buf[ret - 1] ^= 0xFF; + test_tls13_null_bad_hmac_trigger = 0; + } + return ret; +} +#endif + +/* F-2916: TLS 1.3 integrity-only decryption must surface DECRYPT_ERROR + * when the HMAC tag is corrupted. */ +int test_tls13_null_cipher_bad_hmac(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_NULL_CIPHER) && \ + defined(BUILD_TLS_SHA256_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + const char msg[] = "integrity only"; + char recvBuf[32]; + int ret; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_ciphers = test_ctx.s_ciphers = "TLS13-SHA256-SHA256"; + + 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); + + wolfSSL_SSLSetIORecv(ssl_s, test_tls13_null_bad_hmac_io_recv); + + ExpectIntEQ(wolfSSL_write(ssl_c, msg, (int)XSTRLEN(msg)), + (int)XSTRLEN(msg)); + + test_tls13_null_bad_hmac_trigger = 1; + ret = wolfSSL_read(ssl_s, recvBuf, sizeof(recvBuf)); + ExpectIntLE(ret, 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, ret), + WC_NO_ERR_TRACE(DECRYPT_ERROR)); + + test_tls13_null_bad_hmac_trigger = 0; + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index fb8a23d1a2..275888adf0 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -25,6 +25,7 @@ int test_tls_ems_downgrade(void); int test_tls_ems_resumption_downgrade(void); int test_tls12_chacha20_poly1305_bad_tag(void); +int test_tls13_null_cipher_bad_hmac(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From 3c446c3180da7c715bb119827560181572ee70cd Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 14:58:21 +0200 Subject: [PATCH 08/12] tests: add SCR verify_data mismatch test (F-2913, F-2914) Cover both branches of TLSX_SecureRenegotiation_Parse's ConstantCompare against the cached Finished verify_data: a single memio test loops over client-side and server-side corruption, renegotiates, and asserts the offending peer surfaces SECURE_RENEGOTIATION_E. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 66 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 68 insertions(+) diff --git a/tests/api.c b/tests/api.c index 8624423b4e..82c5e43488 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36301,6 +36301,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls_ems_resumption_downgrade), TEST_DECL(test_tls12_chacha20_poly1305_bad_tag), TEST_DECL(test_tls13_null_cipher_bad_hmac), + TEST_DECL(test_scr_verify_data_mismatch), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 976a3d9490..9c2368ef2e 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -284,6 +284,72 @@ int test_tls13_null_cipher_bad_hmac(void) } +/* F-2913 and F-2914: the TLSX_SecureRenegotiation_Parse + * ConstantCompare against the cached Finished verify_data must reject + * a mismatch on both the client and server sides. */ +int test_scr_verify_data_mismatch(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SECURE_RENEGOTIATION) && !defined(WOLFSSL_NO_TLS12) && \ + defined(BUILD_TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + int side; + + for (side = 0; side < 2; side++) { + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL *failing; + byte data; + int ret; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.c_ciphers = test_ctx.s_ciphers = + "ECDHE-RSA-AES128-GCM-SHA256"; + + 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_CTX_UseSecureRenegotiation(ctx_c), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_UseSecureRenegotiation(ctx_s), + WOLFSSL_SUCCESS); + 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); + + /* side 0: corrupt the client's copy; side 1: corrupt the + * server's copy. */ + if (side == 0) { + if (ssl_c != NULL && ssl_c->secure_renegotiation != NULL) + ssl_c->secure_renegotiation->server_verify_data[0] ^= 0xFF; + failing = ssl_c; + } + else { + if (ssl_s != NULL && ssl_s->secure_renegotiation != NULL) + ssl_s->secure_renegotiation->client_verify_data[0] ^= 0xFF; + failing = ssl_s; + } + + ret = wolfSSL_Rehandshake(ssl_c); + (void)ret; + (void)wolfSSL_read(ssl_s, &data, 1); + (void)wolfSSL_read(ssl_c, &data, 1); + ExpectIntEQ(wolfSSL_get_error(failing, 0), + WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + } +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index 275888adf0..bbf3250969 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -26,6 +26,7 @@ int test_tls_ems_downgrade(void); int test_tls_ems_resumption_downgrade(void); int test_tls12_chacha20_poly1305_bad_tag(void); int test_tls13_null_cipher_bad_hmac(void); +int test_scr_verify_data_mismatch(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From b59a53face16b39165fef7a01fdcd34a56cc7130 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 15:04:56 +0200 Subject: [PATCH 09/12] tests: add default ticket key callback HMAC negative test (F-2922) wolfSSL_TicketKeyCb is the built-in ticket callback registered by the OpenSSL-compat wolfSSL_CTX_set_tlsext_ticket_key_cb API. Its ConstantCompare of the ticket HMAC was never reached in any test, so a deletion of the check would silently accept forged tickets. New test sets up the compat callback, establishes a TLS 1.2 session, saves it, flips a byte of the encrypted ticket, and asserts the resumption attempt does not complete. --- tests/api.c | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/api.c b/tests/api.c index 82c5e43488..f3ab04ad63 100644 --- a/tests/api.c +++ b/tests/api.c @@ -10436,6 +10436,78 @@ static int test_wolfSSL_SCR_check_enabled(void) return EXPECT_RESULT(); } +/* F-2922: wolfSSL_TicketKeyCb must reject a session ticket whose HMAC + * does not match its encrypted contents. */ +static int test_wolfSSL_ticket_keycb_bad_hmac(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_TLS12) && \ + defined(OPENSSL_EXTRA) && defined(HAVE_AES_CBC) && \ + defined(WOLFSSL_AES_256) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *session = NULL; + + 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(OpenSSLTicketInit(), 0); + ExpectIntEQ(wolfSSL_CTX_set_tlsext_ticket_key_cb(ctx_s, + myTicketEncCbOpenSSL), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSessionTicket(ssl_c), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(session = wolfSSL_get1_session(ssl_c)); + ExpectIntGT(session->ticketLen, 0); + + /* Corrupt a byte of the ticket HMAC so the server's HMAC + * verification rejects it. */ + if (session != NULL && session->ticket != NULL && session->ticketLen > 0) + session->ticket[session->ticketLen - 1] ^= 0xFF; + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + test_memio_clear_buffer(&test_ctx, 0); + test_memio_clear_buffer(&test_ctx, 1); + + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_set_session(ssl_c, session), WOLFSSL_SUCCESS); + /* Disable the process-global session cache lookup on the server so that + * the ticket is the only resumption path - with WOLFSSL_TICKET_HAVE_ID + * the server could otherwise resume by session ID. */ + if (ssl_s != NULL) + ssl_s->options.sessionCacheOff = 1; + + /* Corrupted ticket bytes fail the HMAC check in + * wolfSSL_TicketKeyCb; the session must not resume. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_c), 0); + + wolfSSL_SESSION_free(session); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + OpenSSLTicketCleanup(); +#endif + return EXPECT_RESULT(); +} + + #if !defined(NO_WOLFSSL_SERVER) && !defined(NO_TLS) && \ !defined(NO_FILESYSTEM) && (!defined(NO_RSA) || defined(HAVE_ECC)) /* Called when writing. */ @@ -36310,6 +36382,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_wolfSSL_UseSecureRenegotiation), TEST_DECL(test_wolfSSL_SCR_Reconnect), TEST_DECL(test_wolfSSL_SCR_check_enabled), + TEST_DECL(test_wolfSSL_ticket_keycb_bad_hmac), TEST_DECL(test_tls_ext_duplicate), TEST_DECL(test_tls_bad_legacy_version), #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) From b0e163629847b66fee2b9607407854aeb8a145b4 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 15:06:43 +0200 Subject: [PATCH 10/12] tests: add HRR cipher-suite mismatch negative test (F-2126) DoTls13ClientHello enforces RFC 8446 Section 4.1.4 by comparing the cipher suite in the second ClientHello to the hrrCipherSuite cached on the server from the HelloRetryRequest. No existing test covers the mismatch branch, so a deletion of the check would silently allow a client to switch cipher suite between CH1 and CH2. Drive a partial handshake until the server has emitted the HRR, then flip the cached hrrCipherSuite on the server; processing CH2 must surface INVALID_PARAMETER. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 60 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 62 insertions(+) diff --git a/tests/api.c b/tests/api.c index f3ab04ad63..498b826c36 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36374,6 +36374,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls12_chacha20_poly1305_bad_tag), TEST_DECL(test_tls13_null_cipher_bad_hmac), TEST_DECL(test_scr_verify_data_mismatch), + TEST_DECL(test_tls13_hrr_cipher_suite_mismatch), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 9c2368ef2e..87143fc642 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -350,6 +350,66 @@ int test_scr_verify_data_mismatch(void) return EXPECT_RESULT(); } +/* F-2126: DoTls13ClientHello must reject a second ClientHello whose + * cipher suite does not match the server's HelloRetryRequest. The + * client offers two suites in CH1 and only a different one in CH2. */ +int test_tls13_hrr_cipher_suite_mismatch(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) && \ + defined(BUILD_TLS_AES_128_GCM_SHA256) && \ + defined(BUILD_TLS_AES_256_GCM_SHA384) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + int ret; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* Both suites supported on both ends; server prefers the first + * offered suite, which will be the one committed in the HRR. */ + test_ctx.c_ciphers = test_ctx.s_ciphers = + "TLS13-AES128-GCM-SHA256:TLS13-AES256-GCM-SHA384"; + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + /* Force HRR by withholding key_share entries in CH1. */ + ExpectIntEQ(wolfSSL_NoKeyShares(ssl_c), WOLFSSL_SUCCESS); + + /* CH1 / HRR */ + ExpectIntEQ(wolfSSL_connect(ssl_c), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_c, 0), WOLFSSL_ERROR_WANT_READ); + ExpectIntEQ(wolfSSL_accept(ssl_s), WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR)); + ExpectIntEQ(wolfSSL_get_error(ssl_s, 0), WOLFSSL_ERROR_WANT_READ); + + /* Restrict the client to a different suite than the one the + * server committed to in the HRR, so CH2 offers only that. */ + ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, "TLS13-AES256-GCM-SHA384"), + WOLFSSL_SUCCESS); + + /* CH2 */ + (void)wolfSSL_connect(ssl_c); + (void)wolfSSL_accept(ssl_s); + (void)wolfSSL_connect(ssl_c); + /* The cipher-suite mismatch is caught server-side; the server's + * alert reaches the client, so either peer can surface it. */ + ret = wolfSSL_get_error(ssl_s, 0); + if (ret != WC_NO_ERR_TRACE(INVALID_PARAMETER)) + ret = wolfSSL_get_error(ssl_c, 0); + ExpectIntEQ(ret, WC_NO_ERR_TRACE(INVALID_PARAMETER)); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index bbf3250969..ad87b3365f 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -27,6 +27,7 @@ int test_tls_ems_resumption_downgrade(void); int test_tls12_chacha20_poly1305_bad_tag(void); int test_tls13_null_cipher_bad_hmac(void); int test_scr_verify_data_mismatch(void); +int test_tls13_hrr_cipher_suite_mismatch(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From 35e674c8173ab287f62446208f6887daf7c20119 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 15 Apr 2026 15:08:44 +0200 Subject: [PATCH 11/12] tests: add TLS 1.3 ticket age out-of-window test (F-1824) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DoClientTicketCheck's ticket-age bounds (-1000 ms low bound and MAX_TICKET_AGE_DIFF*1000+1000 ms high bound) were never exercised by any integration test, so mutations of the constants went undetected. Establish a TLS 1.3 session, read the NewSessionTicket, then shift the client's cached ageAdd by well over 1 second so the server's unobfuscated diff falls outside the valid window on resumption. The server must reject the PSK — session_reused stays 0. --- tests/api.c | 1 + tests/api/test_tls_ext.c | 63 ++++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 1 + 3 files changed, 65 insertions(+) diff --git a/tests/api.c b/tests/api.c index 498b826c36..e763f09784 100644 --- a/tests/api.c +++ b/tests/api.c @@ -36375,6 +36375,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_tls13_null_cipher_bad_hmac), TEST_DECL(test_scr_verify_data_mismatch), TEST_DECL(test_tls13_hrr_cipher_suite_mismatch), + TEST_DECL(test_tls13_ticket_age_out_of_window), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), TEST_DECL(test_certificate_authorities_certificate_request), TEST_DECL(test_certificate_authorities_client_hello), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 87143fc642..1fcd7e7de7 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -410,6 +410,69 @@ int test_tls13_hrr_cipher_suite_mismatch(void) } +/* F-1824: DoClientTicketCheck must reject a PSK whose obfuscated age + * falls outside the [-1000, MAX_TICKET_AGE_DIFF*1000+1000] ms window. */ +int test_tls13_ticket_age_out_of_window(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_TLS13) && defined(HAVE_SESSION_TICKET) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + WOLFSSL_SESSION *session = NULL; + byte tmp; + + 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); + + /* Pump post-handshake reads so the NewSessionTicket reaches the + * client. */ + (void)wolfSSL_read(ssl_c, &tmp, sizeof(tmp)); + (void)wolfSSL_read(ssl_s, &tmp, sizeof(tmp)); + (void)wolfSSL_read(ssl_c, &tmp, sizeof(tmp)); + + ExpectNotNull(session = wolfSSL_get1_session(ssl_c)); + + /* Flip the high bit to push the unobfuscated age out of window. */ + if (session != NULL) + session->ticketAdd ^= 0x80000000U; + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + test_memio_clear_buffer(&test_ctx, 0); + test_memio_clear_buffer(&test_ctx, 1); + + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + ExpectIntEQ(wolfSSL_set_session(ssl_c, session), WOLFSSL_SUCCESS); + + /* PSK rejected, full handshake must still succeed. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_session_reused(ssl_s), 0); + + wolfSSL_SESSION_free(session); + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + + int test_wolfSSL_DisableExtendedMasterSecret(void) { EXPECT_DECLS; diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index ad87b3365f..7d0921c9ad 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -28,6 +28,7 @@ int test_tls12_chacha20_poly1305_bad_tag(void); int test_tls13_null_cipher_bad_hmac(void); int test_scr_verify_data_mismatch(void); int test_tls13_hrr_cipher_suite_mismatch(void); +int test_tls13_ticket_age_out_of_window(void); int test_wolfSSL_DisableExtendedMasterSecret(void); int test_certificate_authorities_certificate_request(void); int test_certificate_authorities_client_hello(void); From a14dfa37b861f81ed7b44a69acb957b204668351 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Mon, 20 Apr 2026 17:45:55 +0000 Subject: [PATCH 12/12] fenrir: address review feedback on PR 10230 - tls.c: TLSX_CertWithExternPsk_GetSize takes word16*, but length was widened to word32 in TLSX_GetSize. Use the hsz staging variable like the other cases so WOLFSSL_CERT_WITH_EXTERN_PSK builds compile. - tls.c: silence -Wunused-variable for hsz in builds where every case that consumes it (TLS 1.3, PSK, ETM, early data, PHA, cookie, cert with extern PSK) is compiled out, e.g. user_settings_tls12.h. - test_tls_ext.c: assert session->ticketLen > 0 before mutating ticketAdd in the ticket-age out-of-window test so it fails loudly if no NewSessionTicket was received. --- src/tls.c | 6 +++++- tests/api/test_tls_ext.c | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/tls.c b/src/tls.c index 506237bf1a..dc24bc75bf 100644 --- a/src/tls.c +++ b/src/tls.c @@ -14880,6 +14880,8 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, byte isRequest = (msgType == client_hello || msgType == certificate_request); + (void)hsz; + while ((extension = list)) { list = extension->next; @@ -14998,7 +15000,9 @@ static int TLSX_GetSize(TLSX* list, byte* semaphore, byte msgType, break; #ifdef WOLFSSL_CERT_WITH_EXTERN_PSK case TLSX_CERT_WITH_EXTERN_PSK: - ret = PSK_WITH_CERT_GET_SIZE(msgType, &length); + hsz = 0; + ret = PSK_WITH_CERT_GET_SIZE(msgType, &hsz); + length += hsz; break; #endif #endif diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 1fcd7e7de7..d183bfe6db 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -439,6 +439,9 @@ int test_tls13_ticket_age_out_of_window(void) (void)wolfSSL_read(ssl_c, &tmp, sizeof(tmp)); ExpectNotNull(session = wolfSSL_get1_session(ssl_c)); + /* The test only exercises the age window check if the client actually + * received a NewSessionTicket and the session carries ticket material. */ + ExpectIntGT(session->ticketLen, 0); /* Flip the high bit to push the unobfuscated age out of window. */ if (session != NULL)