From a32d92e9eb3a84838168972783dfeb250b637b2d Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 3 Jun 2026 17:27:54 -0700 Subject: [PATCH 01/12] 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 bf0e047365..b1d29e76eb 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3357,6 +3357,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 4705b7204198b0105049cea89cffd5606d839cf2 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 3 Jun 2026 17:29:30 -0700 Subject: [PATCH 02/12] Check for ticket expiration before using a ticket for resumption. Thanks to Ben Smyth for the report. --- src/internal.c | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index 114e4138ab..f4080fe4a2 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34606,17 +34606,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, - ssl->session->ticketLen, ssl->heap); - 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; +#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; - idSz = 0; + ret = TLSX_UseSessionTicket(&ssl->extensions, ticket, + ssl->heap); + if (ret != WOLFSSL_SUCCESS) { + TLSX_SessionTicket_Free(ticket, ssl->heap); + return ret; + } + + idSz = 0; + } } #endif /* HAVE_SESSION_TICKET */ length = VERSION_SZ + RAN_LEN From 1fade8d2f753c9002b462d2ba9121d636697e428 Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 4 Jun 2026 15:58:58 -0700 Subject: [PATCH 03/12] 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 f4080fe4a2..ccd6695b9e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34635,9 +34635,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 dd27f119a7ac7e471f1f50f83fb048c475e390a1 Mon Sep 17 00:00:00 2001 From: Kareem Date: Mon, 8 Jun 2026 10:38:20 -0700 Subject: [PATCH 04/12] 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 ccd6695b9e..92381d798c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34616,8 +34616,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 335114d16a2257466e45e1f1cc4728f6b7ce8aa0 Mon Sep 17 00:00:00 2001 From: Kareem Date: Mon, 15 Jun 2026 10:08:13 -0700 Subject: [PATCH 05/12] 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 92381d798c..e2fbddaa22 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34608,18 +34608,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 && @@ -34628,6 +34620,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 b1d29e76eb..7797fdb260 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3362,8 +3362,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 fdeb2ea30204e6730aafdffab8cd4d8c32caa4a2 Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 22 Jul 2026 13:47:29 -0700 Subject: [PATCH 06/12] 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 e2fbddaa22..6ecfca8536 100644 --- a/src/internal.c +++ b/src/internal.c @@ -4053,6 +4053,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; @@ -4066,6 +4075,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 777336230c..653717ae29 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2826,10 +2826,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, @@ -2839,8 +2844,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 88a7559158..6d2f14ffd9 100644 --- a/tests/test-dtls13.conf +++ b/tests/test-dtls13.conf @@ -271,26 +271,6 @@ -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 - # server DTLSv1.3 no (EC)DHE with PSK, must still key share in the HRR -u -v 4 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 3ad1a95217f966beaf52d187c21777a802e993dd Mon Sep 17 00:00:00 2001 From: Kareem Date: Wed, 22 Jul 2026 16:06:56 -0700 Subject: [PATCH 07/12] 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 653717ae29..cdfd06215e 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2842,17 +2842,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 baf82e2f0632ad6a7dff2283417c68b08696bbcb Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 23 Jul 2026 16:14:54 -0700 Subject: [PATCH 08/12] 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 25e01d94ea..af12aa3bb1 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -1157,6 +1157,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_CHECK_PSK_MODES WOLFSSL_TLS13_TICKET_BEFORE_FINISHED From 46854d9491513a2055c07624029a96cddbf20edc Mon Sep 17 00:00:00 2001 From: Kareem Date: Tue, 11 Aug 2026 13:21:06 -0700 Subject: [PATCH 09/12] Code review feedback: Added tests Restored NULL cipher suite tests into new conf files Restored previous eNULL handling Fix session timeout check and clear ticket when not resuming Document WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT --- .github/configs/os-check-linux.json | 4 + src/internal.c | 55 ++++++++--- src/tls13.c | 5 + tests/api.c | 145 ++++++++++++++++++++++------ tests/include.am | 2 + tests/suites.c | 27 ++++++ wolfssl/internal.h | 4 + 7 files changed, 199 insertions(+), 43 deletions(-) diff --git a/.github/configs/os-check-linux.json b/.github/configs/os-check-linux.json index fb44231a80..fdaeb4df16 100644 --- a/.github/configs/os-check-linux.json +++ b/.github/configs/os-check-linux.json @@ -87,6 +87,10 @@ {"name": "dtls13-ocspstapling-cert-cb", "minutes": 3.1, "configure": ["--enable-dtls", "--enable-dtls13", "--enable-ocspstapling", "--enable-ocspstapling2", "--enable-cert-setup-cb", "--enable-sessioncerts"]}, +{"name": "nullcipher-tls13-in-default", "minutes": 3.0, + "comment": "Legacy WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT mode: RFC 9150 integrity-only suites back in the default TLS 1.3/DTLS 1.3 cipher list. The only config that runs tests/test-tls13-null.conf and tests/test-dtls13-null.conf (they need the suites in the default list). The dtls-cid-renego-psk entry covers the default-exclusion mode of --enable-nullcipher.", + "configure": ["--enable-dtls", "--enable-dtls13", "--enable-nullcipher", + "CPPFLAGS=-DWOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT -DWOLFSSL_STATIC_RSA"]}, {"name": "tsp-verifier", "minutes": 3, "comment": "Time-Stamp Protocol Verifier", "configure": ["--enable-tsp", "--enable-opensslall", diff --git a/src/internal.c b/src/internal.c index 6ecfca8536..7e237cd743 100644 --- a/src/internal.c +++ b/src/internal.c @@ -97,6 +97,8 @@ * WOLFSSL_TICKET_ENC_CBC_HMAC: * CBC+HMAC for ticket encryption (non-AEAD) default: off * WOLFSSL_NO_TICKET_EXPIRE: Disable ticket expiration checking default: off + * (server-side resumption and the client-side check + * on a stored ticket before offering it) * * TLS 1.3 Internals: * WOLFSSL_TLS13_IGNORE_PT_ALERT_ON_ENC: @@ -106,6 +108,9 @@ * WOLFSSL_TLS13_IGNORE_AEAD_LIMITS: * Ignore AEAD message limits from RFC 9846 5.5, which * makes observing them a MUST default: off + * WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT: + * Include RFC 9150 integrity-only suites in the + * default cipher suite list (HAVE_NULL_CIPHER) default: off * WOLFSSL_DTLS13_SEND_MOREACK_DEFAULT: * Send more ACKs by default in DTLS 1.3 default: off * @@ -4057,25 +4062,31 @@ static word16 InitSuites_Tls13(Suites* suites, word16 idx, int tls1_3, * 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: + * explicitly with a cipher list, either by suite name: * 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 + * or with the "eNULL" keyword. Define + * WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT to restore the legacy behaviour of + * including them in the default list. */ + #ifndef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT + if (haveNull == SUITES_NULL_EXPLICIT) + #else + if (haveNull) + #endif + { #ifdef BUILD_TLS_SHA256_SHA256 - if (tls1_3 && haveNull) { + if (tls1_3) { suites->suites[idx++] = ECC_BYTE; suites->suites[idx++] = TLS_SHA256_SHA256; } #endif #ifdef BUILD_TLS_SHA384_SHA384 - if (tls1_3 && haveNull) { + if (tls1_3) { suites->suites[idx++] = ECC_BYTE; suites->suites[idx++] = TLS_SHA384_SHA384; } #endif - #endif /* WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT */ + } #endif return idx; @@ -32165,7 +32176,7 @@ static int ParseCipherList(Suites* suites, } if (XSTRCMP(name, "eNULL") == 0 || XSTRCMP(name, "NULL") == 0) { - haveNull = allowing; + haveNull = allowing ? SUITES_NULL_EXPLICIT : 0; /* Track exclusion (sticky) so an explicit NULL-cipher suite is * dropped at the end regardless of "!eNULL"/"!NULL" position; a * later allowing "eNULL" does not undo it. */ @@ -34618,11 +34629,11 @@ 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. Drop the expired + * NOT use a ticket whose lifetime has expired. Delete 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 && + * bornOn or timeout is 0 (unknown lifetime) or a secret callback + * is set (session is managed externally, e.g. hostap). */ + if (ssl->session->bornOn != 0 && ssl->session->timeout != 0 && #ifdef HAVE_SECRET_CALLBACK ssl->sessionSecretCb == NULL && #endif @@ -34630,6 +34641,16 @@ 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; + /* RFC 5077 Section 3.3: delete the ticket and associated + * state. */ + ForceZero(ssl->session->ticket, ssl->session->ticketLen); + if (ssl->session->ticketLenAlloc > 0) { + XFREE(ssl->session->ticket, NULL, + DYNAMIC_TYPE_SESSION_TICK); + ssl->session->ticket = ssl->session->staticTicket; + ssl->session->ticketLenAlloc = 0; + } + ssl->session->ticketLen = 0; /* Send an empty SessionTicket extension (NULL ticket) so the * client still requests a new ticket from the server without * sending the stale one. */ @@ -34641,7 +34662,8 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) #endif { ticket = TLSX_SessionTicket_Create(0, ssl->session->ticket, - ssl->session->ticketLen, ssl->heap); + ssl->session->ticketLen, + ssl->heap); if (ticket == NULL) return MEMORY_E; ret = TLSX_UseSessionTicket(&ssl->extensions, ticket, @@ -41393,9 +41415,10 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) * Handles session resumption. * Session tickets are checked for validity based on the time each ticket * was created, timeout value and the current time. If the tickets are - * judged expired, falls back to full-handshake. If you want disable this - * session ticket validation check in TLS1.2 and below, define - * WOLFSSL_NO_TICKET_EXPIRE. + * judged expired, falls back to full-handshake. If you want disable + * this session ticket validation check in TLS1.2 and below (both here + * and the client-side check on a stored ticket in SendClientHello), + * define WOLFSSL_NO_TICKET_EXPIRE. */ int HandleTlsResumption(WOLFSSL* ssl, Suites* clSuites) { diff --git a/src/tls13.c b/src/tls13.c index 0bf97c982a..d64879e113 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -69,6 +69,11 @@ * WOLFSSL_CHECK_SIG_FAULTS: Verify signature after ECC signing default: off * to detect fault injection attacks * WOLFSSL_CIPHER_TEXT_CHECK: Verify ciphertext integrity default: off + * WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT: Include RFC 9150 suites default: off + * in the default cipher suite list (requires + * HAVE_NULL_CIPHER). Without it the integrity-only + * suites must be requested explicitly in the + * cipher list, by name or with "eNULL". * * TLS 1.3 PSK: * WOLFSSL_PSK_ONE_ID: Single PSK identity per connect default: off diff --git a/tests/api.c b/tests/api.c index cdfd06215e..d28b7f1c14 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2826,15 +2826,11 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) wolfSSL_free(ssl); ssl = NULL; - /* 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. */ + /* The RFC 9150 integrity-only TLS 1.3 suites are zero-confidentiality: + * they are kept out of the default preference list (unless + * WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT is defined) and are never produced + * by "ALL" alone, but an explicit "eNULL" keyword or suite name requests + * them in either mode. */ ExpectNotNull(ssl = wolfSSL_new(ctx)); ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL"), WOLFSSL_SUCCESS); ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, @@ -2842,28 +2838,18 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) wolfSSL_free(ssl); ssl = NULL; - { -#ifdef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT - const int eNullExpectsSuite = 1; -#else - const int eNullExpectsSuite = 0; -#endif - 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; - } + 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), 1); /* explicit "eNULL" requests them */ + 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); + TLS_SHA256_SHA256), 1); /* explicit suite name works too */ wolfSSL_free(ssl); ssl = NULL; #endif /* BUILD_TLS_SHA256_SHA256 */ @@ -2881,6 +2867,41 @@ static int test_wolfSSL_set_cipher_list_exclusions(void) #undef TEST_CIPHER_EXCLUDE_NULL #endif +/* A client using the default cipher list must not offer the RFC 9150 + * integrity-only TLS 1.3 suites unless WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT + * is defined. The server accepts only TLS13-SHA256-SHA256, so the handshake + * outcome shows whether the default list contained it. */ +static int test_tls13_null_cipher_default_list(void) +{ + EXPECT_DECLS; +#if defined(BUILD_TLS_SHA256_SHA256) && !defined(NO_RSA) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + struct test_memio_ctx test_ctx; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* Client keeps the default cipher list. */ + 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); +#ifdef WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(ssl_c->options.cipherSuite0, ECC_BYTE); + ExpectIntEQ(ssl_c->options.cipherSuite, TLS_SHA256_SHA256); +#else + /* no common suite: the server rejects the ClientHello */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), -1); + ExpectIntEQ(ssl_s->error, WC_NO_ERR_TRACE(MATCH_SUITE_ERROR)); +#endif + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + static int test_wolfSSL_set_alpn_protos_default_fails(void) { EXPECT_DECLS; @@ -31512,8 +31533,15 @@ static int test_export_keying_material_cb(WOLFSSL_CTX *ctx, WOLFSSL *ssl) static int test_export_keying_material_ssl_cb(WOLFSSL* ssl) { + EXPECT_DECLS; + byte ekm[32] = {0}; + wolfSSL_KeepArrays(ssl); - return TEST_SUCCESS; + /* Export must be refused until the handshake has completed. */ + ExpectIntEQ(wolfSSL_export_keying_material(ssl, ekm, sizeof(ekm), + "Test label", XSTR_SIZEOF("Test label"), NULL, 0, 1), + WOLFSSL_FAILURE); + return EXPECT_RESULT(); } static int test_export_keying_material(void) @@ -35692,6 +35720,67 @@ static int test_ticket_ret_create(void) } #endif +/* RFC 5077 Section 3.3: a stored ticket whose lifetime has expired must not + * be offered; the client falls back to a full handshake. */ +static int test_ticket_expired_full_handshake(void) +{ + EXPECT_DECLS; +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_TLS12) && \ + !defined(WOLFSSL_NO_TICKET_EXPIRE) && !defined(NO_ASN_TIME) && \ + !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) && !defined(NO_RSA) && \ + defined(HAVE_ECC) && defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) + WOLFSSL_CTX *ctx_c = NULL; + WOLFSSL_CTX *ctx_s = NULL; + WOLFSSL *ssl_c = NULL; + WOLFSSL *ssl_s = NULL; + struct test_memio_ctx test_ctx; + WOLFSSL_SESSION *sess = NULL; + int i; + + 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); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, 0); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, 0); + ExpectIntEQ(wolfSSL_CTX_UseSessionTicket(ctx_c), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + + /* Round 0: control, the ticket resumes as-is. Round 1: same ticket made + * to look expired; the client must do a full handshake instead. */ + for (i = 0; i < 2; i++) { + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + ExpectNotNull(ssl_s = wolfSSL_new(ctx_s)); + wolfSSL_SetIOWriteCtx(ssl_s, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_s, &test_ctx); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + wolfSSL_SetIOWriteCtx(ssl_c, &test_ctx); + wolfSSL_SetIOReadCtx(ssl_c, &test_ctx); + + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + if (i == 1 && ssl_c != NULL) { + ssl_c->session->bornOn -= ssl_c->session->timeout + 1; + } + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + if (ssl_c != NULL) { + ExpectIntEQ(ssl_c->options.resuming, i == 0 ? 1 : 0); + } + } + + 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(); +} + /* Build a valid TLS 1.2 ticket by completing an initial handshake, then tamper * with enc_len so it is larger than the true encrypted payload. */ #if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_TLS12) && \ @@ -42036,6 +42125,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_set_cipher_list_tls12_with_version), TEST_DECL(test_wolfSSL_set_cipher_list_tls13_with_version), TEST_DECL(test_wolfSSL_set_cipher_list_exclusions), + TEST_DECL(test_tls13_null_cipher_default_list), TEST_DECL(test_wolfSSL_set_alpn_protos_default_fails), TEST_DECL(test_wolfSSL_CTX_use_certificate), TEST_DECL(test_wolfSSL_CTX_use_certificate_file), @@ -42324,6 +42414,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_ticket_nonce_malloc), #endif TEST_DECL(test_ticket_ret_create), + TEST_DECL(test_ticket_expired_full_handshake), TEST_DECL(test_ticket_enc_corrupted), TEST_DECL(test_wrong_cs_downgrade), TEST_DECL(test_tls13_no_ext_sh_alert), diff --git a/tests/include.am b/tests/include.am index 038042ba62..ab5272b2a5 100644 --- a/tests/include.am +++ b/tests/include.am @@ -37,6 +37,7 @@ EXTRA_DIST += tests/unit.h \ tests/test-tls13.conf \ tests/test-tls13-down.conf \ tests/test-tls13-ecc.conf \ + tests/test-tls13-null.conf \ tests/test-tls13-psk.conf \ tests/test-tls13-pq-standalone.conf \ tests/test-tls13-pq-hybrid.conf \ @@ -71,6 +72,7 @@ EXTRA_DIST += tests/unit.h \ tests/test-dtls-srtp.conf \ tests/test-dtls-srtp-fails.conf \ tests/test-dtls13.conf \ + tests/test-dtls13-null.conf \ tests/test-dtls13-downgrade.conf \ tests/test-dtls13-downgrade-fails.conf \ tests/test-dtls13-psk.conf \ diff --git a/tests/suites.c b/tests/suites.c index f63de1d288..34ca279fb9 100644 --- a/tests/suites.c +++ b/tests/suites.c @@ -1180,6 +1180,20 @@ int SuiteTest(int argc, char** argv) goto exit; } #endif + #if defined(HAVE_NULL_CIPHER) && \ + defined(WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT) + /* add TLSv13 integrity-only suites; the harness repeats each case with + * the default cipher list on one side, so these can only pass when the + * default list includes the integrity-only suites */ + XSTRLCPY(argv0[1], "tests/test-tls13-null.conf", sizeof(argv0[1])); + printf("starting TLSv13 integrity-only cipher suite tests\n"); + test_harness(&args); + if (args.return_code != 0) { + printf("error from script %d\n", args.return_code); + args.return_code = EXIT_FAILURE; + goto exit; + } + #endif #ifndef WOLFSSL_NO_TLS12 /* add TLSv13 downgrade tests */ XSTRLCPY(argv0[1], "tests/test-tls13-down.conf", sizeof(argv0[1])); @@ -1651,6 +1665,19 @@ int SuiteTest(int argc, char** argv) goto exit; } +#if defined(HAVE_NULL_CIPHER) && \ + defined(WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT) + /* see the test-tls13-null.conf note on the default-cipher-list repeat */ + XSTRLCPY(argv0[1], "tests/test-dtls13-null.conf", sizeof(argv0[1])); + printf("starting DTLSv1.3 integrity-only cipher suite tests\n"); + test_harness(&args); + if (args.return_code != 0) { + printf("error from script %d\n", args.return_code); + args.return_code = EXIT_FAILURE; + goto exit; + } +#endif /* HAVE_NULL_CIPHER && WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT */ + #ifndef WOLFSSL_NO_TLS12 args.argc = 2; strcpy(argv0[1], "tests/test-dtls13-downgrade.conf"); diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 11ce9e5820..34b0e76653 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -1940,6 +1940,10 @@ WOLFSSL_LOCAL int NamedGroupIsPqcHybrid(int group); /* 150 suites for now! */ #endif +/* InitSuites() haveNull value used when NULL suites are requested explicitly + * (cipher list "eNULL" keyword) rather than merely allowed by default (1). */ +#define SUITES_NULL_EXPLICIT 2 + /* number of items in the signature algo list */ #ifndef WOLFSSL_MAX_SIGALGO #if (defined(WOLFSSL_LEANPSK) || defined(WOLFSSL_LEANTLS)) && \ From bd2e8d9e799c24558fce7afcc23eabd599811ba0 Mon Sep 17 00:00:00 2001 From: Kareem Date: Tue, 11 Aug 2026 14:15:37 -0700 Subject: [PATCH 10/12] Add missing NULL suite conf files. --- tests/test-dtls13-null.conf | 24 ++++++++++++++++++++++++ tests/test-tls13-null.conf | 20 ++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/test-dtls13-null.conf create mode 100644 tests/test-tls13-null.conf diff --git a/tests/test-dtls13-null.conf b/tests/test-dtls13-null.conf new file mode 100644 index 0000000000..538cdaa434 --- /dev/null +++ b/tests/test-dtls13-null.conf @@ -0,0 +1,24 @@ +# DTLSv1.3 RFC 9150 integrity-only suites. Run only in HAVE_NULL_CIPHER builds +# that also define WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT: the harness repeats +# each case with the default cipher list on one side, which can only negotiate +# these suites when they are in the default list. + +# 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 DTLSv1.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-null.conf b/tests/test-tls13-null.conf new file mode 100644 index 0000000000..a0e807a151 --- /dev/null +++ b/tests/test-tls13-null.conf @@ -0,0 +1,20 @@ +# TLSv1.3 RFC 9150 integrity-only suites. Run only in HAVE_NULL_CIPHER builds +# that also define WOLFSSL_TLS13_NULL_CIPHER_IN_DEFAULT: the harness repeats +# each case with the default cipher list on one side, which can only negotiate +# these suites when they are in the default list. + +# 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 01661f08f8c070f57a980e05087c9fb06d3b9298 Mon Sep 17 00:00:00 2001 From: Kareem Date: Thu, 27 Aug 2026 16:49:43 -0700 Subject: [PATCH 11/12] Code review feedback --- src/internal.c | 39 +++++++++--------- src/ssl.c | 7 +++- tests/api.c | 107 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 26 deletions(-) diff --git a/src/internal.c b/src/internal.c index 7e237cd743..27b758b9db 100644 --- a/src/internal.c +++ b/src/internal.c @@ -27641,32 +27641,38 @@ int SendFinished(WOLFSSL* ssl) AddSession(ssl); #endif if (ssl->options.side == WOLFSSL_SERVER_END) { + /* Mark the handshake done before the info callback so the + * callback (e.g. on WOLFSSL_CB_HANDSHAKE_DONE) can use APIs that + * require a completed handshake, such as + * wolfSSL_export_keying_material(). */ + ssl->options.handShakeState = HANDSHAKE_DONE; + ssl->options.handShakeDone = 1; +#ifdef HAVE_SECURE_RENEGOTIATION + ssl->options.resumed = ssl->options.resuming; +#endif #ifdef OPENSSL_EXTRA ssl->options.serverState = SERVER_FINISHED_COMPLETE; ssl->cbmode = WOLFSSL_CB_MODE_WRITE; if (ssl->CBIS != NULL) ssl->CBIS(ssl, WOLFSSL_CB_HANDSHAKE_DONE, WOLFSSL_SUCCESS); #endif + } + } + else { + if (ssl->options.side == WOLFSSL_CLIENT_END) { + /* Same ordering as the server side above: flags first so the + * info callback sees a completed handshake. */ ssl->options.handShakeState = HANDSHAKE_DONE; ssl->options.handShakeDone = 1; #ifdef HAVE_SECURE_RENEGOTIATION ssl->options.resumed = ssl->options.resuming; #endif - } - } - else { - if (ssl->options.side == WOLFSSL_CLIENT_END) { #ifdef OPENSSL_EXTRA ssl->options.clientState = CLIENT_FINISHED_COMPLETE; ssl->cbmode = WOLFSSL_CB_MODE_WRITE; if (ssl->CBIS != NULL) ssl->CBIS(ssl, WOLFSSL_CB_HANDSHAKE_DONE, WOLFSSL_SUCCESS); #endif - ssl->options.handShakeState = HANDSHAKE_DONE; - ssl->options.handShakeDone = 1; -#ifdef HAVE_SECURE_RENEGOTIATION - ssl->options.resumed = ssl->options.resuming; -#endif } } @@ -34629,7 +34635,7 @@ 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. Delete the expired + * NOT use a ticket whose lifetime has expired. Drop the expired * ticket and fall back to a full handshake. Skip the check when * bornOn or timeout is 0 (unknown lifetime) or a secret callback * is set (session is managed externally, e.g. hostap). */ @@ -34641,16 +34647,9 @@ 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; - /* RFC 5077 Section 3.3: delete the ticket and associated - * state. */ - ForceZero(ssl->session->ticket, ssl->session->ticketLen); - if (ssl->session->ticketLenAlloc > 0) { - XFREE(ssl->session->ticket, NULL, - DYNAMIC_TYPE_SESSION_TICK); - ssl->session->ticket = ssl->session->staticTicket; - ssl->session->ticketLenAlloc = 0; - } - ssl->session->ticketLen = 0; + /* The stale ticket stays on the session object, which may be + * shared with the application; the replacement ticket from + * the full handshake overwrites it in SetTicket(). */ /* Send an empty SessionTicket extension (NULL ticket) so the * client still requests a new ticket from the server without * sending the stale one. */ diff --git a/src/ssl.c b/src/ssl.c index 7797fdb260..993edc5879 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -7580,9 +7580,12 @@ long wolfSSL_set_options(WOLFSSL* ssl, long op) * options limit the allowed ciphers so let's try to get as many as * possible. * - haveStaticECC turns off haveRSA - * - haveECDSAsig turns off haveRSAsig */ + * - haveECDSAsig turns off haveRSAsig + * - SUITES_NULL_EXPLICIT includes the integrity-only suites, so + * an explicitly configured one is preserved */ InitSuites(&tmpSuites, ssl->version, 0, 1, 1, 1, haveECDSAsig, 1, 1, - haveStaticECC, 1, 1, 1, 1, 1, ssl->options.side); + haveStaticECC, 1, SUITES_NULL_EXPLICIT, 1, 1, 1, + ssl->options.side); for (in = 0, out = 0; in < ssl->suites->suiteSz; in += SUITE_LEN) { if (FindSuite(&tmpSuites, ssl->suites->suites[in], ssl->suites->suites[in+1]) >= 0) { diff --git a/tests/api.c b/tests/api.c index d28b7f1c14..387712f959 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2672,11 +2672,12 @@ static int test_wolfSSL_set_cipher_list_tls13_with_version(void) static int test_suites_contains(WOLFSSL* ssl, byte s0, byte s1) { int i; - if (ssl == NULL || ssl->suites == NULL) + const Suites* suites = (ssl != NULL) ? WOLFSSL_SUITES(ssl) : NULL; + if (suites == NULL) return 0; - for (i = 0; (i + 1) < ssl->suites->suiteSz; i += 2) { - if ((ssl->suites->suites[i] == s0) && - (ssl->suites->suites[i + 1] == s1)) + for (i = 0; (i + 1) < suites->suiteSz; i += 2) { + if ((suites->suites[i] == s0) && + (suites->suites[i + 1] == s1)) return 1; } return 0; @@ -2902,6 +2903,42 @@ static int test_tls13_null_cipher_default_list(void) return EXPECT_RESULT(); } +/* An explicitly configured integrity-only suite must survive + * wolfSSL_[CTX_]set_options() and wolfSSL_new(): the preserve-overlap + * rebuild in wolfSSL_set_options() must keep explicitly requested NULL + * suites even though they are not in the default list. */ +static int test_tls13_null_cipher_explicit_keep(void) +{ + EXPECT_DECLS; +#if defined(BUILD_TLS_SHA256_SHA256) && defined(OPENSSL_EXTRA) && \ + !defined(NO_WOLFSSL_CLIENT) + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* ctx-level cipher list set before ctx-level options */ + ExpectNotNull(ctx = wolfSSL_CTX_new(wolfSSLv23_client_method())); + ExpectIntEQ(wolfSSL_CTX_set_cipher_list(ctx, "TLS13-SHA256-SHA256"), + WOLFSSL_SUCCESS); + wolfSSL_CTX_set_options(ctx, WOLFSSL_OP_NO_SSLv3); + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, TLS_SHA256_SHA256), 1); + wolfSSL_free(ssl); + ssl = NULL; + + /* ssl-level options after an explicit ssl-level cipher list */ + ExpectNotNull(ssl = wolfSSL_new(ctx)); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "TLS13-SHA256-SHA256"), + WOLFSSL_SUCCESS); + wolfSSL_set_options(ssl, WOLFSSL_OP_NO_SSLv3); + ExpectIntEQ(test_suites_contains(ssl, ECC_BYTE, TLS_SHA256_SHA256), 1); + wolfSSL_free(ssl); + ssl = NULL; + + wolfSSL_CTX_free(ctx); +#endif + return EXPECT_RESULT(); +} + static int test_wolfSSL_set_alpn_protos_default_fails(void) { EXPECT_DECLS; @@ -31510,6 +31547,18 @@ static int test_export_keying_material_cb(WOLFSSL_CTX *ctx, WOLFSSL *ssl) /* Use some random context */ ExpectIntEQ(wolfSSL_export_keying_material(ssl, ekm, sizeof(ekm), "Test label", XSTR_SIZEOF("Test label"), ekm, 10, 1), 1); + /* The handshake-complete gate must be the deciding factor: clearing + * handShakeDone on this otherwise fully established connection flips an + * identical call from success to failure, and restoring it flips it + * back. */ + ssl->options.handShakeDone = 0; + ExpectIntEQ(wolfSSL_export_keying_material(ssl, ekm, sizeof(ekm), + "Test label", XSTR_SIZEOF("Test label"), NULL, 0, 1), + WOLFSSL_FAILURE); + ssl->options.handShakeDone = 1; + ExpectIntEQ(wolfSSL_export_keying_material(ssl, ekm, sizeof(ekm), + "Test label", XSTR_SIZEOF("Test label"), NULL, 0, 1), + WOLFSSL_SUCCESS); /* Failure cases */ ExpectIntEQ(wolfSSL_export_keying_material(ssl, ekm, sizeof(ekm), "client finished", XSTR_SIZEOF("client finished"), NULL, 0, 0), 0); @@ -31559,6 +31608,54 @@ static int test_export_keying_material(void) return EXPECT_RESULT(); } + +#if defined(OPENSSL_EXTRA) && !defined(WOLFSSL_NO_TLS12) +static int test_ekm_info_cb_result = -1; + +static void test_ekm_info_cb(const WOLFSSL* ssl, int type, int val) +{ + byte ekm[32]; + (void)val; + if (type == WOLFSSL_CB_HANDSHAKE_DONE) { + test_ekm_info_cb_result = wolfSSL_export_keying_material( + (WOLFSSL*)ssl, ekm, sizeof(ekm), + "Test label", XSTR_SIZEOF("Test label"), NULL, 0, 1); + } +} + +static int test_ekm_info_ctx_ready(WOLFSSL_CTX* ctx) +{ + wolfSSL_CTX_set_info_callback(ctx, test_ekm_info_cb); + return TEST_SUCCESS; +} +#endif /* OPENSSL_EXTRA && !WOLFSSL_NO_TLS12 */ + +/* wolfSSL_export_keying_material() must work from inside the + * WOLFSSL_CB_HANDSHAKE_DONE info callback: the handshake-done flags are set + * before the callback fires (TLS 1.2 SendFinished, server side here). */ +static int test_export_keying_material_info_cb(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && !defined(WOLFSSL_NO_TLS12) + test_ssl_cbf serverCb; + test_ssl_cbf clientCb; + + XMEMSET(&serverCb, 0, sizeof(serverCb)); + XMEMSET(&clientCb, 0, sizeof(clientCb)); + clientCb.method = wolfTLSv1_2_client_method; + serverCb.method = wolfTLSv1_2_server_method; + serverCb.ctx_ready = test_ekm_info_ctx_ready; + /* exporter needs the handshake arrays kept on the exporting side */ + serverCb.ssl_ready = test_export_keying_material_ssl_cb; + + test_ekm_info_cb_result = -1; + ExpectIntEQ(test_wolfSSL_client_server_nofail_memio(&clientCb, + &serverCb, NULL), TEST_SUCCESS); + /* The callback fired and the export succeeded. */ + ExpectIntEQ(test_ekm_info_cb_result, WOLFSSL_SUCCESS); +#endif + return EXPECT_RESULT(); +} #endif /* HAVE_KEYING_MATERIAL */ static int test_wolfSSL_THREADID_hash(void) @@ -42126,6 +42223,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_set_cipher_list_tls13_with_version), TEST_DECL(test_wolfSSL_set_cipher_list_exclusions), TEST_DECL(test_tls13_null_cipher_default_list), + TEST_DECL(test_tls13_null_cipher_explicit_keep), TEST_DECL(test_wolfSSL_set_alpn_protos_default_fails), TEST_DECL(test_wolfSSL_CTX_use_certificate), TEST_DECL(test_wolfSSL_CTX_use_certificate_file), @@ -42364,6 +42462,7 @@ TEST_CASE testCases[] = { #if defined(HAVE_KEYING_MATERIAL) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) TEST_DECL(test_export_keying_material), + TEST_DECL(test_export_keying_material_info_cb), #endif /* Can't memory test as client/server Asserts in thread. */ From ea6e1ec252f7eaf86f4b815139703293e7c0269e Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 11 Sep 2026 11:50:12 -0700 Subject: [PATCH 12/12] Code review feedback: Avoid underflow in ticket time comparison. Ensure test_suite_contains is built for the SHA256_SHA256 test. --- src/internal.c | 4 ++-- tests/api.c | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/internal.c b/src/internal.c index 27b758b9db..d3893e5608 100644 --- a/src/internal.c +++ b/src/internal.c @@ -34643,8 +34643,8 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) #ifdef HAVE_SECRET_CALLBACK ssl->sessionSecretCb == NULL && #endif - LowResTimer() >= - (ssl->session->bornOn + ssl->session->timeout)) { + (LowResTimer() - ssl->session->bornOn) >= + ssl->session->timeout) { WOLFSSL_MSG("Stored session ticket expired; full handshake"); ssl->options.resuming = 0; /* The stale ticket stays on the session object, which may be diff --git a/tests/api.c b/tests/api.c index 387712f959..d91b1359c3 100644 --- a/tests/api.c +++ b/tests/api.c @@ -2667,9 +2667,12 @@ static int test_wolfSSL_set_cipher_list_tls13_with_version(void) #endif #endif -#if defined(TEST_CIPHER_EXCLUDE_ANON) || defined(TEST_CIPHER_EXCLUDE_NULL) -/* Does the parsed suite list on ssl contain the given suite bytes? */ -static int test_suites_contains(WOLFSSL* ssl, byte s0, byte s1) +#if defined(TEST_CIPHER_EXCLUDE_ANON) || defined(TEST_CIPHER_EXCLUDE_NULL) || \ + defined(BUILD_TLS_SHA256_SHA256) +/* Does the parsed suite list on ssl contain the given suite bytes? + * WC_MAYBE_UNUSED: the TLS 1.3 null-cipher tests gate further inside their + * bodies, so some builds compile this helper without a caller. */ +static WC_MAYBE_UNUSED int test_suites_contains(WOLFSSL* ssl, byte s0, byte s1) { int i; const Suites* suites = (ssl != NULL) ? WOLFSSL_SUITES(ssl) : NULL;