From daa6d2fd008471d83950733ea4aabe63470da06d Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 3 Jun 2026 17:22:39 -0700 Subject: [PATCH 1/9] Also send decode_error for BUFFER_E as this error code is used throughout tls.c/tls13.c. Thanks to Ben Smyth for the report. --- src/internal.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internal.c b/src/internal.c index 5914aa4203..7edbf85856 100644 --- a/src/internal.c +++ b/src/internal.c @@ -36928,6 +36928,7 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, { switch (err) { case WC_NO_ERR_TRACE(BUFFER_ERROR): + case WC_NO_ERR_TRACE(BUFFER_E): return decode_error; case WC_NO_ERR_TRACE(EXT_NOT_ALLOWED): case WC_NO_ERR_TRACE(PEER_KEY_ERROR): From a60e49a4487201966e60228840b386e38d42bb65 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 3 Jun 2026 17:27:54 -0700 Subject: [PATCH 2/9] Prevent exporting keying material until the handshake is complete. Thanks to Ben Smyth for the report. --- src/ssl.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ssl.c b/src/ssl.c index 47d731221f..25171c7d76 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3326,6 +3326,17 @@ int wolfSSL_export_keying_material(WOLFSSL *ssl, return WOLFSSL_FAILURE; } + /* RFC 8446 Section 7.5 / RFC 5705: keying-material exporters derive from + * exporter_master_secret, which exists only after the handshake is + * complete. Refuse the export until the handshake has completed so that + * a premature call cannot derive material from an uninitialised + * exporterSecret buffer. */ + if (ssl->options.handShakeDone == 0 || + ssl->options.handShakeState != HANDSHAKE_DONE) { + WOLFSSL_MSG("Handshake not complete; refusing keying-material export"); + return WOLFSSL_FAILURE; + } + /* Sanity check contextLen to prevent integer overflow when cast to word32 * and to ensure it fits in the 2-byte length encoding (max 65535). */ if (use_context && contextLen > WOLFSSL_MAX_16BIT) { From 0c6f741620854accacecefa25e9897985163ef3f Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 3 Jun 2026 17:29:30 -0700 Subject: [PATCH 3/9] Check for ticket expiration before using a ticket for resumption. Thanks to Ben Smyth for the report. --- src/internal.c | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/internal.c b/src/internal.c index 7edbf85856..fcd5846cfc 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32682,17 +32682,38 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) if (ssl->options.resuming && ssl->session->ticketLen > 0) { SessionTicket* ticket; - ticket = TLSX_SessionTicket_Create(0, ssl->session->ticket, +#if !defined(WOLFSSL_NO_TICKET_EXPIRE) && !defined(NO_ASN_TIME) + /* RFC 5077 Section 3.3 / RFC 8446 Section 4.6.1: a client SHOULD + * NOT use a ticket whose lifetime has expired. If the stored + * session has aged past its timeout the server would just reject + * the resumption, so suppress the ticket here and fall back to a + * full handshake (avoids leaking a stale ticket and saves a + * round-trip). Expiry is measured against ssl->session->timeout + * (the session's own lifetime) so this stays consistent with + * wolfSSL_SetSession(), which gates resumption on the same field; + * keying off ssl->timeout instead could contradict a decision + * SetSession() already made when the two values differ. */ + if (LowResTimer() >= + (ssl->session->bornOn + ssl->session->timeout)) { + WOLFSSL_MSG("Stored session ticket expired; full handshake"); + ssl->options.resuming = 0; + } + else +#endif + { + ticket = TLSX_SessionTicket_Create(0, ssl->session->ticket, ssl->session->ticketLen, ssl->heap); - if (ticket == NULL) return MEMORY_E; + if (ticket == NULL) return MEMORY_E; - ret = TLSX_UseSessionTicket(&ssl->extensions, ticket, ssl->heap); - if (ret != WOLFSSL_SUCCESS) { - TLSX_SessionTicket_Free(ticket, ssl->heap); - return ret; - } + ret = TLSX_UseSessionTicket(&ssl->extensions, ticket, + ssl->heap); + if (ret != WOLFSSL_SUCCESS) { + TLSX_SessionTicket_Free(ticket, ssl->heap); + return ret; + } - idSz = 0; + idSz = 0; + } } #endif /* HAVE_SESSION_TICKET */ length = VERSION_SZ + RAN_LEN From 7e0e1bef19643615f6ad32162d4d6456c3ed3175 Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 4 Jun 2026 15:58:58 -0700 Subject: [PATCH 4/9] Code review feedback: set idSz = 0 for both cases. --- src/internal.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index fcd5846cfc..f28ffe57d9 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32711,9 +32711,8 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) TLSX_SessionTicket_Free(ticket, ssl->heap); return ret; } - - idSz = 0; } + idSz = 0; } #endif /* HAVE_SESSION_TICKET */ length = VERSION_SZ + RAN_LEN From a45de563356c629b2582412c4ec888301db8a46b Mon Sep 17 00:00:00 2001 From: Kareem Date: Mon, 8 Jun 2026 10:38:20 -0700 Subject: [PATCH 5/9] Skip session timeout check if bornOn is 0 or if a secret callback is set. This should fix hostap EAP-FAST failures. --- src/internal.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index f28ffe57d9..eceb43cab6 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32692,8 +32692,15 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) * (the session's own lifetime) so this stays consistent with * wolfSSL_SetSession(), which gates resumption on the same field; * keying off ssl->timeout instead could contradict a decision - * SetSession() already made when the two values differ. */ - if (LowResTimer() >= + * SetSession() already made when the two values differ. + * If bornOn is 0 or the secret callback is set, it is assumed that + * the session is being externally managed and this check is + * skipped. This is needed for hostap. */ + if (ssl->session->bornOn != 0 && + #ifdef HAVE_SECRET_CALLBACK + ssl->sessionSecretCb == NULL && + #endif + LowResTimer() >= (ssl->session->bornOn + ssl->session->timeout)) { WOLFSSL_MSG("Stored session ticket expired; full handshake"); ssl->options.resuming = 0; From 90ba2b92cdc10efa61752a588bb88da501905402 Mon Sep 17 00:00:00 2001 From: Kareem Date: Mon, 15 Jun 2026 10:08:13 -0700 Subject: [PATCH 6/9] Code review feedback --- src/internal.c | 22 ++++++++++------------ src/ssl.c | 3 +-- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/internal.c b/src/internal.c index eceb43cab6..a73aa09427 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32684,18 +32684,10 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) #if !defined(WOLFSSL_NO_TICKET_EXPIRE) && !defined(NO_ASN_TIME) /* RFC 5077 Section 3.3 / RFC 8446 Section 4.6.1: a client SHOULD - * NOT use a ticket whose lifetime has expired. If the stored - * session has aged past its timeout the server would just reject - * the resumption, so suppress the ticket here and fall back to a - * full handshake (avoids leaking a stale ticket and saves a - * round-trip). Expiry is measured against ssl->session->timeout - * (the session's own lifetime) so this stays consistent with - * wolfSSL_SetSession(), which gates resumption on the same field; - * keying off ssl->timeout instead could contradict a decision - * SetSession() already made when the two values differ. - * If bornOn is 0 or the secret callback is set, it is assumed that - * the session is being externally managed and this check is - * skipped. This is needed for hostap. */ + * NOT use a ticket whose lifetime has expired. Drop the expired + * ticket and fall back to a full handshake. Skip the check when + * bornOn is 0 or a secret callback is set (session is managed + * externally, e.g. hostap). */ if (ssl->session->bornOn != 0 && #ifdef HAVE_SECRET_CALLBACK ssl->sessionSecretCb == NULL && @@ -32704,6 +32696,12 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) (ssl->session->bornOn + ssl->session->timeout)) { WOLFSSL_MSG("Stored session ticket expired; full handshake"); ssl->options.resuming = 0; + /* Send an empty SessionTicket extension (NULL ticket) so the + * client still requests a new ticket from the server without + * sending the stale one. */ + ret = TLSX_UseSessionTicket(&ssl->extensions, NULL, ssl->heap); + if (ret != WOLFSSL_SUCCESS) + return ret; } else #endif diff --git a/src/ssl.c b/src/ssl.c index 25171c7d76..a0b4ea6f15 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3331,8 +3331,7 @@ int wolfSSL_export_keying_material(WOLFSSL *ssl, * complete. Refuse the export until the handshake has completed so that * a premature call cannot derive material from an uninitialised * exporterSecret buffer. */ - if (ssl->options.handShakeDone == 0 || - ssl->options.handShakeState != HANDSHAKE_DONE) { + if (ssl->options.handShakeDone == 0) { WOLFSSL_MSG("Handshake not complete; refusing keying-material export"); return WOLFSSL_FAILURE; } From 77186c31f45e3620c7f1c8644921c67539ba87f3 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 22 Jul 2026 13:47:29 -0700 Subject: [PATCH 7/9] Avoid advertising NULL cipher suites by default unless WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT is defined. Thanks to Ben Smyth for the report. --- src/internal.c | 10 ++++++++++ tests/api.c | 30 +++++++++++++++++++++++++----- tests/test-dtls13.conf | 20 -------------------- tests/test-tls13.conf | 16 ---------------- 4 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/internal.c b/src/internal.c index a73aa09427..fe41a3a486 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3731,6 +3731,15 @@ static word16 InitSuites_Tls13(Suites* suites, word16 idx, int tls1_3, #endif #ifdef HAVE_NULL_CIPHER + /* RFC 9150 integrity-only (zero-confidentiality) TLS 1.3 suites. + * These provide authentication and integrity but no confidentiality, so + * they are NOT advertised in the default cipher preference list. A caller + * that genuinely needs them (e.g. constrained/IoT deployments) must opt in + * explicitly with a cipher list, for example: + * wolfSSL_set_cipher_list(ssl, "TLS13-SHA256-SHA256"); + * Define WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT to restore the legacy + * behaviour of including them in the default list. */ + #ifdef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT #ifdef BUILD_TLS_SHA256_SHA256 if (tls1_3 && haveNull) { suites->suites[idx++] = ECC_BYTE; @@ -3744,6 +3753,7 @@ static word16 InitSuites_Tls13(Suites* suites, word16 idx, int tls1_3, suites->suites[idx++] = TLS_SHA384_SHA384; } #endif + #endif /* WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT */ #endif return idx; diff --git a/tests/api.c b/tests/api.c index 7cf9225e49..ffea8b472e 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2512,10 +2512,15 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) wolfSSL_free(ssl); ssl = NULL; - /* OpenSSL compat: "ALL" is "all but eNULL" - it does not generate NULL - * suites, but it is not a delete directive, so a following "eNULL" - * re-enables them. (The earlier sticky excludeNull-for-ALL wrongly kept - * them disabled; only "!eNULL"/"DEFAULT" should.) */ + /* By default the RFC 9150 integrity-only TLS 1.3 suites are + * zero-confidentiality and are treated as SSL_NOT_DEFAULT: + * they are never produced by the generic cipher-string directives ("ALL", + * "eNULL", ...) and are kept out of the default preference list, so they + * must be requested by explicit suite name. + * + * Defining WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT restores the legacy + * behaviour where the "eNULL" directive re-enables them via the generated + * list. "ALL" on its own never generates NULL ciphers in either mode. */ ExpectNotNull(ssl = wolfSSL_new(ctx)); ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL"), WOLFSSL_SUCCESS); ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, @@ -2525,8 +2530,23 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) ExpectNotNull(ssl = wolfSSL_new(ctx)); ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL:eNULL"), WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, TLS_SHA256_SHA256), +#ifdef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT + 1 /* legacy: "eNULL" re-enables the integrity-only suite */ +#else + 0 /* default: SSL_NOT_DEFAULT, "eNULL" does not add it */ +#endif + ); + wolfSSL_free(ssl); + ssl = NULL; + + /* Explicit suite name remains the supported opt-in and works in both + * modes. */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "TLS13-SHA256-SHA256"), + WOLFSSL_SUCCESS); ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, - TLS_SHA256_SHA256), 1); /* "eNULL" after "ALL" re-enables NULL */ + TLS_SHA256_SHA256), 1); wolfSSL_free(ssl); ssl = NULL; #endif /* BUILD_TLS_SHA256_SHA256 */ diff --git a/tests/test-dtls13.conf b/tests/test-dtls13.conf index 57724e68c2..0186b9a7fe 100644 --- a/tests/test-dtls13.conf +++ b/tests/test-dtls13.conf @@ -270,23 +270,3 @@ -u -v 4 -l TLS_AES_128_GCM_SHA256 - -# server DTLSv1.3 Integrity-only SHA256 --u --v 4 --l TLS13-SHA256-SHA256 - -# client DTLSv1.3 Integrity-only SHA256 --u --v 4 --l TLS13-SHA256-SHA256 - -# server DTSv1.3 Integrity-only SHA384 --u --v 4 --l TLS13-SHA384-SHA384 - -# client DTLSv1.3 Integrity-only SHA384 --u --v 4 --l TLS13-SHA384-SHA384 diff --git a/tests/test-tls13.conf b/tests/test-tls13.conf index 266f373214..4b4db1be34 100644 --- a/tests/test-tls13.conf +++ b/tests/test-tls13.conf @@ -213,19 +213,3 @@ # client TLSv1.3 Send Ticket explicitly -v 4 -l TLS13-AES128-GCM-SHA256 - -# server TLSv1.3 Integrity-only SHA256 --v 4 --l TLS13-SHA256-SHA256 - -# client TLSv1.3 Integrity-only SHA256 --v 4 --l TLS13-SHA256-SHA256 - -# server TLSv1.3 Integrity-only SHA384 --v 4 --l TLS13-SHA384-SHA384 - -# client TLSv1.3 Integrity-only SHA384 --v 4 --l TLS13-SHA384-SHA384 From 0e274c2a19a4bd7d9ac3bcd3ba5bd69fa867f000 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 22 Jul 2026 16:06:56 -0700 Subject: [PATCH 8/9] Avoid using preprocessor gate inside of a macro. --- tests/api.c | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/api.c b/tests/api.c index ffea8b472e..19d6d75cad 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2528,17 +2528,20 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) wolfSSL_free(ssl); ssl = NULL; - ExpectNotNull(ssl = wolfSSL_new(ctx)); - ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL:eNULL"), WOLFSSL_SUCCESS); - ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, TLS_SHA256_SHA256), + { #ifdef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT - 1 /* legacy: "eNULL" re-enables the integrity-only suite */ + const int eNullExpectsSuite = 1; #else - 0 /* default: SSL_NOT_DEFAULT, "eNULL" does not add it */ + const int eNullExpectsSuite = 0; #endif - ); - wolfSSL_free(ssl); - ssl = NULL; + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL:eNULL"), + WOLFSSL_SUCCESS); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, TLS_SHA256_SHA256), + eNullExpectsSuite); + wolfSSL_free(ssl); + ssl = NULL; + } /* Explicit suite name remains the supported opt-in and works in both * modes. */ From 2c30081db15af54566931f69f7153e7425e93c3c Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 23 Jul 2026 16:14:54 -0700 Subject: [PATCH 9/9] Add WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT to known macros. --- .wolfssl_known_macro_extras | 1 + 1 file changed, 1 insertion(+) diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index e71e4a00c7..bdf5e56be2 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1031,6 +1031,7 @@ WOLFSSL_TI_CURRTIME WOLFSSL_TLS13_DRAFT WOLFSSL_TLS13_IGNORE_AEAD_LIMITS WOLFSSL_TLS13_IGNORE_PT_ALERT_ON_ENC +WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT WOLFSSL_TLS13_SHA512 WOLFSSL_TLS13_TICKET_BEFORE_FINISHED WOLFSSL_TLSX_PQC_MLKEM_STORE_PRIV_KEY