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 diff --git a/src/internal.c b/src/internal.c index 5914aa4203..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; @@ -32682,16 +32692,41 @@ 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. 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 && + #endif + LowResTimer() >= + (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 + { + 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; } #endif /* HAVE_SESSION_TICKET */ @@ -36928,6 +36963,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): diff --git a/src/ssl.c b/src/ssl.c index 47d731221f..a0b4ea6f15 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -3326,6 +3326,16 @@ 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) { + 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) { diff --git a/tests/api.c b/tests/api.c index 7cf9225e49..19d6d75cad 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, @@ -2523,10 +2528,28 @@ 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; + } + + /* Explicit suite name remains the supported opt-in and works in both + * modes. */ ExpectNotNull(ssl = wolfSSL_new(ctx)); - ExpectIntEQ(wolfSSL_set_cipher_list(ssl, "ALL:eNULL"), WOLFSSL_SUCCESS); + 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