From ae497ef59e7d93bab2ae53290f9a3d3a41ddb419 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:23:53 +0200 Subject: [PATCH 01/10] F-3888: add negative tests for tampered RSA signatures/hashes Extend test_wolfSSL_RSA_verify and test_wolfSSL_RSA_padding_add_PKCS1_PSS with negative cases that flip a byte in the signature/encoding and in the hash, asserting verification fails. This guards the XMEMCMP-based signature acceptance decision in wolfSSL_RSA_verify_mgf against regressions that would let any decryption result of matching length pass as valid. --- tests/api/test_ossl_rsa.c | 40 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/tests/api/test_ossl_rsa.c b/tests/api/test_ossl_rsa.c index 26cf360105f..dc0cee665ba 100644 --- a/tests/api/test_ossl_rsa.c +++ b/tests/api/test_ossl_rsa.c @@ -520,6 +520,24 @@ int test_wolfSSL_RSA_padding_add_PKCS1_PSS(void) ExpectIntEQ(RSA_verify_PKCS1_PSS(rsa, mHash, EVP_sha256(), em, RSA_PSS_SALTLEN_DIGEST), 1); + /* Negative test: a tampered PSS encoding must be rejected. Flip a byte in + * the encoded message, confirm failure, then restore and re-verify. */ + em[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify_PKCS1_PSS(rsa, mHash, EVP_sha256(), em, + RSA_PSS_SALTLEN_DIGEST), 0); + em[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify_PKCS1_PSS(rsa, mHash, EVP_sha256(), em, + RSA_PSS_SALTLEN_DIGEST), 1); + + /* Negative test: a tampered hash must be rejected by PSS verification. */ + { + unsigned char badHash[WC_SHA256_DIGEST_SIZE]; + XMEMCPY(badHash, mHash, sizeof(badHash)); + badHash[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify_PKCS1_PSS(rsa, badHash, EVP_sha256(), em, + RSA_PSS_SALTLEN_DIGEST), 0); + } + ExpectIntEQ(RSA_padding_add_PKCS1_PSS(rsa, em, mHash, EVP_sha256(), RSA_PSS_SALTLEN_MAX_SIGN), 1); ExpectIntEQ(RSA_verify_PKCS1_PSS(rsa, mHash, EVP_sha256(), em, @@ -696,8 +714,8 @@ int test_wolfSSL_RSA_verify(void) RSA *pubKey = NULL; X509 *cert = NULL; const char *text = "Hello wolfSSL !"; - unsigned char hash[SHA256_DIGEST_LENGTH]; - unsigned char signature[2048/8]; + unsigned char hash[SHA256_DIGEST_LENGTH] = {0}; + unsigned char signature[2048/8] = {0}; unsigned int signatureLength; byte *buf = NULL; BIO *bio = NULL; @@ -747,6 +765,24 @@ int test_wolfSSL_RSA_verify(void) ExpectIntEQ(RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH, signature, signatureLength, pubKey), SSL_SUCCESS); + /* Negative test: a tampered signature must be rejected. Flip a byte in the + * signature, confirm verification fails, then restore it. */ + signature[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH, signature, + signatureLength, pubKey), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + signature[0] ^= 0xFFU; + /* Sanity: the restored signature verifies again. */ + ExpectIntEQ(RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH, signature, + signatureLength, pubKey), SSL_SUCCESS); + + /* Negative test: a tampered hash must be rejected (the encoded comparison + * string differs). Flip a byte in the hash, confirm failure, then + * restore it. */ + hash[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH, signature, + signatureLength, pubKey), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); + hash[0] ^= 0xFFU; + ExpectIntEQ(RSA_verify(NID_sha256, NULL, SHA256_DIGEST_LENGTH, NULL, signatureLength, NULL), WC_NO_ERR_TRACE(WOLFSSL_FAILURE)); ExpectIntEQ(RSA_verify(NID_sha256, NULL, SHA256_DIGEST_LENGTH, signature, From 40e6dccce6fdedf816823e53049f130a2cc24bc1 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:24:25 +0200 Subject: [PATCH 02/10] F-4867: reject trailing bytes in TLS 1.3 EncryptedExtensions DoTls13EncryptedExtensions only bounds-checked the extensions length against the message size, silently ignoring any trailing bytes. RFC 8446 Section 4.3.1 defines the message as solely the extensions block, so enforce length equality and return BUFFER_ERROR (decode_error) on a mismatch. --- src/tls13.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tls13.c b/src/tls13.c index 8d7efb9df40..7b8838c992a 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6021,7 +6021,7 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input, i += OPAQUE16_LEN; /* Extension data. */ - if (i - begin + totalExtSz > totalSz) + if (i - begin + totalExtSz != totalSz) return BUFFER_ERROR; if ((ret = TLSX_Parse(ssl, input + i, totalExtSz, encrypted_extensions, NULL))) { From 4ba0737fe934fadbb90b3c8a62cd1f5d5fdea4df Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:24:46 +0200 Subject: [PATCH 03/10] F-4868: reject trailing bytes in TLS 1.3 CertificateRequest DoTls13CertificateRequest advanced past the certificate_request_context and extensions blocks but never verified the whole message body was consumed, silently ignoring trailing bytes. RFC 8446 Section 4.3.2 fixes the wire format; enforce that the consumed length equals the message size and return BUFFER_ERROR (decode_error) otherwise. --- src/tls13.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tls13.c b/src/tls13.c index 7b8838c992a..df78e1be2e6 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6134,6 +6134,10 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, } *inOutIdx += len; + /* No trailing bytes allowed (RFC 8446 4.3.2). */ + if ((*inOutIdx - begin) != size) + return BUFFER_ERROR; + #ifdef WOLFSSL_CERT_SETUP_CB if ((ret = CertSetupCbWrapper(ssl)) != 0) return ret; From d6e56d49439890fcbf4617e8b3180bfabb96b7cc Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:25:17 +0200 Subject: [PATCH 04/10] F-5633: zeroize DTLS 1.3 ChaCha record-number keys before free FreeCiphers released the DTLS 1.3 record-number protection ChaCha contexts with XFREE only, leaving key material in freed heap memory. ForceZero both contexts before freeing, matching the regular TLS ChaCha path in FreeCiphersSide, and also zeroize a partially-set key in Dtls13InitChaChaCipher when wc_Chacha_SetKey fails. --- src/dtls13.c | 1 + src/internal.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/dtls13.c b/src/dtls13.c index 995d144119f..d0ad01c801c 100644 --- a/src/dtls13.c +++ b/src/dtls13.c @@ -2225,6 +2225,7 @@ static int Dtls13InitChaChaCipher(RecordNumberCiphers* c, byte* key, ret = wc_Chacha_SetKey(c->chacha, key, keySize); if (ret != 0) { + ForceZero(c->chacha, sizeof(ChaCha)); XFREE(c->chacha, heap, DYNAMIC_TYPE_CIPHER); c->chacha = NULL; } diff --git a/src/internal.c b/src/internal.c index 45a1f809fc9..1bc5681ff73 100644 --- a/src/internal.c +++ b/src/internal.c @@ -3304,6 +3304,10 @@ void FreeCiphers(WOLFSSL* ssl) ssl->dtlsRecordNumberDecrypt.aes = NULL; #endif /* BUILD_AES */ #ifdef HAVE_CHACHA + if (ssl->dtlsRecordNumberEncrypt.chacha) + ForceZero(ssl->dtlsRecordNumberEncrypt.chacha, sizeof(ChaCha)); + if (ssl->dtlsRecordNumberDecrypt.chacha) + ForceZero(ssl->dtlsRecordNumberDecrypt.chacha, sizeof(ChaCha)); XFREE(ssl->dtlsRecordNumberEncrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER); XFREE(ssl->dtlsRecordNumberDecrypt.chacha, ssl->heap, DYNAMIC_TYPE_CIPHER); ssl->dtlsRecordNumberEncrypt.chacha = NULL; From 5c51f574c0f4399005c1bd5c98c903751dcc8554 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:25:46 +0200 Subject: [PATCH 05/10] F-5807: enforce EMS consistency on client session resumption CompleteServerHello's resumption branch derived keys from the cached master secret without checking the resumed session's extended_master_secret state against the abbreviated ServerHello, letting a MITM strip EMS on resumption. Per RFC 7627 Section 5.3, abort with a fatal handshake_failure when the cached session's EMS flag does not match the ServerHello EMS state. --- src/internal.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/internal.c b/src/internal.c index 1bc5681ff73..30b5e79eeb5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32203,6 +32203,22 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) } else { if (DSH_CheckSessionId(ssl)) { + /* RFC 7627 5.3: resumed session EMS state must match the + * ServerHello; abort on mismatch. Stateless (session-ticket) + * resumption - e.g. EAP-FAST, whose PAC is a TLS ticket - binds + * the EMS state in the ticket and need not re-advertise the + * extension, so this applies only to session-ID resumption. */ + if ( + #ifdef HAVE_SESSION_TICKET + ssl->session->ticketLen == 0 && + #endif + ssl->session->haveEMS != ssl->options.haveEMS) { + WOLFSSL_MSG("Resumed session EMS state does not match " + "ServerHello EMS state"); + SendAlert(ssl, alert_fatal, handshake_failure); + WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E); + return EXT_MASTER_SECRET_NEEDED_E; + } if (SetCipherSpecs(ssl) == 0) { if (!HaveUniqueSessionObj(ssl)) { WOLFSSL_MSG("Unable to have unique session object"); From c5bbfe1a8415f55006665b35a86e6dba8a153fe9 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:26:18 +0200 Subject: [PATCH 06/10] F-5811: verify resumed cipher suite matches cached session On session-ID resumption the client only checked that the server's selected suite was in its offered list, not that it equaled the resumed session's suite, so a server could resume the session ID under a different cipher suite. Per RFC 5246 Section 7.4.1.2 / F.1.4 a resumed session reuses its negotiated suite; abort with a fatal illegal_parameter on mismatch. --- src/internal.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/internal.c b/src/internal.c index 30b5e79eeb5..d3fe7f54c74 100644 --- a/src/internal.c +++ b/src/internal.c @@ -32219,6 +32219,25 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) WOLFSSL_ERROR_VERBOSE(EXT_MASTER_SECRET_NEEDED_E); return EXT_MASTER_SECRET_NEEDED_E; } +#ifndef NO_RESUME_SUITE_CHECK + /* RFC 5246 Section 7.4.1.3: on resumption the ServerHello + * reuses the previously negotiated cipher suite. Reject a + * server that resumes the session but selects a different + * suite. Skipped for ticket resumption (suite is bound in the + * ticket), consistent with the EMS check above. */ + if ( + #ifdef HAVE_SESSION_TICKET + ssl->session->ticketLen == 0 && + #endif + (ssl->options.cipherSuite0 != ssl->session->cipherSuite0 || + ssl->options.cipherSuite != ssl->session->cipherSuite)) { + WOLFSSL_MSG("Resumed session cipher suite does not match " + "ServerHello cipher suite"); + SendAlert(ssl, alert_fatal, illegal_parameter); + WOLFSSL_ERROR_VERBOSE(MATCH_SUITE_ERROR); + return MATCH_SUITE_ERROR; + } +#endif /* NO_RESUME_SUITE_CHECK */ if (SetCipherSpecs(ssl) == 0) { if (!HaveUniqueSessionObj(ssl)) { WOLFSSL_MSG("Unable to have unique session object"); From a8a65748ebd07f359f2d407a22d5c0046cbbbc58 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:27:25 +0200 Subject: [PATCH 07/10] F-5813: fail TLS 1.2 record send before the sequence number wraps GetSEQIncrement silently rolled the 64-bit write sequence counter from 2^64-1 back to 0, reusing sequence number 0 with the same keys. Per RFC 5246 Section 6.1 sequence numbers MUST NOT wrap. BuildMessage now refuses to emit a TLS 1.2 record once the write sequence number has reached its maximum, returning the new SEQUENCE_NUMBER_E error so the caller renegotiates or closes instead. --- src/internal.c | 16 ++++++++++++++++ wolfssl/error-ssl.h | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index d3fe7f54c74..511c63d07c5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -24602,6 +24602,19 @@ int BuildMessage(WOLFSSL* ssl, byte* output, int outSz, const byte* input, #endif #ifndef WOLFSSL_NO_TLS12 + /* RFC 5246 6.1: record sequence numbers MUST NOT wrap. Refuse to emit a + * record once the write sequence number has reached its maximum value + * (2^64-1); reusing sequence number 0 with the same keys would break the + * record protection. The caller must renegotiate or close the connection + * instead. DTLS sequence numbers are epoch-scoped and handled elsewhere. */ + if (!sizeOnly && !ssl->options.dtls && + ssl->keys.sequence_number_hi == 0xFFFFFFFFU && + ssl->keys.sequence_number_lo == 0xFFFFFFFFU) { + WOLFSSL_MSG("TLS write sequence number would wrap"); + WOLFSSL_ERROR_VERBOSE(SEQUENCE_NUMBER_E); + return SEQUENCE_NUMBER_E; + } + #ifdef WOLFSSL_ASYNC_CRYPT ret = WC_NO_PENDING_E; if (asyncOkay) { @@ -28048,6 +28061,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e) case ECH_REQUIRED_E: return "ECH offered but rejected by server"; + + case SEQUENCE_NUMBER_E: + return "Record sequence number would wrap"; } return "unknown error number"; diff --git a/wolfssl/error-ssl.h b/wolfssl/error-ssl.h index b98a52d40c9..379008afb01 100644 --- a/wolfssl/error-ssl.h +++ b/wolfssl/error-ssl.h @@ -244,7 +244,9 @@ enum wolfSSL_ErrorCodes { ECH_REQUIRED_E = -519, /* ECH offered but rejected by server */ - WOLFSSL_LAST_E = -519 + SEQUENCE_NUMBER_E = -520, /* Record sequence number would wrap */ + + WOLFSSL_LAST_E = -520 /* codes -1000 to -1999 are reserved for wolfCrypt. */ }; From 146da72a419b0c4e3b9a758c80fbf4262cadc467 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:28:26 +0200 Subject: [PATCH 08/10] F-5818: invalidate cached session on fatal alert DoAlert marked a connection closed on a received fatal alert but left the established session in the resumption cache, and the send path did the same, so a session whose connection ended in a fatal alert remained resumable. Per RFC 5246 Section 7.2.2 the session identifier MUST be invalidated; evict the established session from the cache on both receipt and transmission of a fatal alert via the new InvalidateSessionOnFatalAlert helper. --- src/internal.c | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/internal.c b/src/internal.c index 511c63d07c5..f519f339107 100644 --- a/src/internal.c +++ b/src/internal.c @@ -22447,6 +22447,24 @@ static void LogAlert(int type) } /* process alert, return level */ +#ifndef NO_SESSION_CACHE +/* RFC 5246 Section 7.2.2: a TLS 1.2 session whose connection is terminated by a + * fatal alert MUST be invalidated so it cannot be resumed. (TLS 1.3 RFC 8446 + * Section 6.2 only requires closing the connection, but evicting here too is + * sound defense-in-depth.) Evict the cached session (which also drops any + * associated ticket). Acts on an established connection or an in-progress + * resumption - both reference a cached session; a brand-new full handshake has + * no cached session to remove. */ +static void InvalidateSessionOnFatalAlert(WOLFSSL* ssl) +{ + if (ssl == NULL || ssl->ctx == NULL || ssl->session == NULL) + return; + if (!ssl->options.handShakeDone && !ssl->options.resuming) + return; + (void)wolfSSL_SSL_CTX_remove_session(ssl->ctx, ssl->session); +} +#endif /* !NO_SESSION_CACHE */ + static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) { byte level; @@ -22496,6 +22514,15 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) code != close_notify && code != user_canceled) { ssl->options.isClosed = 1; } +#ifndef NO_SESSION_CACHE + /* A fatal alert immediately terminates the connection; invalidate the + * session so it cannot be used to establish new connections. In TLS 1.3 + * all error alerts are implicitly fatal (RFC 8446 6.2). */ + if (code != close_notify && + (level == alert_fatal || + (IsAtLeastTLSv1_3(ssl->version) && code != user_canceled))) + InvalidateSessionOnFatalAlert(ssl); +#endif } if (++ssl->options.alertCount >= WOLFSSL_ALERT_COUNT_MAX) { @@ -27379,6 +27406,17 @@ int SendAlert(WOLFSSL* ssl, int severity, int type) return BAD_FUNC_ARG; } + /* InvalidateSessionOnFatalAlert() is defined in the !NO_TLS section, so the + * guard here must match (with NO_TLS there are no TLS sessions to evict). */ +#if !defined(NO_SESSION_CACHE) && !defined(NO_TLS) + /* RFC 5246 Section 7.2.2: a fatal alert terminates the connection; + * invalidate the established session so it cannot be resumed. Do this as + * soon as the fatal alert is generated, before the pendingAlert/backpressure + * handling below which can return early without sending the alert now. */ + if (severity == alert_fatal) + InvalidateSessionOnFatalAlert(ssl); +#endif + if (ssl->pendingAlert.level != alert_none) { ret = RetrySendAlert(ssl); if (ret != 0) { From c1097dc2ebca20ae55e8fa4346682e9c28836382 Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:29:19 +0200 Subject: [PATCH 09/10] F-4144: honor WOLFSSL_OP_NO_RENEGOTIATION The documented 'reject peer-initiated renegotiation' option was accepted and stored but never consulted. Now DoHelloRequest replies with a no_renegotiation warning instead of starting SCR when the bit is set (client side), and the server refuses a renegotiation ClientHello with a no_renegotiation warning instead of resetting handshake state. --- src/internal.c | 21 ++++++ tests/api.c | 2 + tests/api/test_tls_ext.c | 139 +++++++++++++++++++++++++++++++++++++++ tests/api/test_tls_ext.h | 2 + 4 files changed, 164 insertions(+) diff --git a/src/internal.c b/src/internal.c index f519f339107..710a8a1c34a 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18009,6 +18009,15 @@ static int DoHelloRequest(WOLFSSL* ssl, word32 size) } #ifdef HAVE_SECURE_RENEGOTIATION else if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) { + /* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting + * peer-initiated renegotiation. Respond with a no_renegotiation + * warning alert instead of starting a secure renegotiation. */ + if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) { + WOLFSSL_MSG("Rejecting HelloRequest: WOLFSSL_OP_NO_RENEGOTIATION"); + WOLFSSL_LEAVE("DoHelloRequest", 0); + WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO); + return SendAlert(ssl, alert_warning, no_renegotiation); + } ssl->secure_renegotiation->startScr = 1; WOLFSSL_LEAVE("DoHelloRequest", 0); WOLFSSL_END(WC_FUNC_HELLO_REQUEST_DO); @@ -18741,6 +18750,17 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) { + /* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting + * peer-initiated renegotiation. RFC 5246 7.2.2: no_renegotiation is a + * warning-level alert, so refuse the renegotiation but keep the + * established connection rather than aborting it. Skip the ClientHello + * body and leave handshake state untouched, mirroring the client-side + * HelloRequest refusal in DoHelloRequest(). */ + if (ssl->options.mask & WOLFSSL_OP_NO_RENEGOTIATION) { + WOLFSSL_MSG("Refusing renegotiation: WOLFSSL_OP_NO_RENEGOTIATION"); + *inOutIdx = expectedIdx; + return SendAlert(ssl, alert_warning, no_renegotiation); + } WOLFSSL_MSG("Reset handshake state"); XMEMSET(&ssl->msgsReceived, 0, sizeof(MsgsReceived)); ssl->options.serverState = NULL_STATE; @@ -23180,6 +23200,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) /* see if sending SSLv2 client hello */ if ( ssl->options.side == WOLFSSL_SERVER_END && ssl->options.clientState == NULL_STATE && + !ssl->options.handShakeDone && ssl->buffers.inputBuffer.buffer[ssl->buffers.inputBuffer.idx] != handshake && /* change_cipher_spec here is an error but we want to handle diff --git a/tests/api.c b/tests/api.c index a2873fd747d..273bcbdc714 100644 --- a/tests/api.c +++ b/tests/api.c @@ -34420,6 +34420,8 @@ 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_scr_no_renegotiation_option), + TEST_DECL(test_helloRequest_no_renegotiation_option), TEST_DECL(test_tls13_hrr_cipher_suite_mismatch), TEST_DECL(test_tls13_ticket_age_out_of_window), TEST_DECL(test_wolfSSL_DisableExtendedMasterSecret), diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 294fa9c903c..7da0976eb4c 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -350,6 +350,145 @@ int test_scr_verify_data_mismatch(void) return EXPECT_RESULT(); } +/* F-4144: WOLFSSL_OP_NO_RENEGOTIATION on the server must refuse a + * client-initiated renegotiation with a no_renegotiation *warning* while + * keeping the established connection alive, rather than aborting it. */ +int test_scr_no_renegotiation_option(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) + 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_ALERT_HISTORY history; + byte readBuf[16]; + int ret = WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR); + int i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&history, 0, sizeof(history)); + 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); + + /* Server opts into rejecting peer-initiated renegotiation. */ + wolfSSL_set_options(ssl_s, WOLFSSL_OP_NO_RENEGOTIATION); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Client initiates renegotiation: it sends a ClientHello and waits for a + * ServerHello that never comes. */ + ExpectIntLT(wolfSSL_Rehandshake(ssl_c), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + /* Server processes the renegotiation ClientHello. It must refuse without + * aborting: the read returns WANT_READ (connection still alive), not a + * SECURE_RENEGOTIATION_E fatal error. */ + ExpectIntLT(wolfSSL_read(ssl_s, readBuf, sizeof(readBuf)), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + + /* The refusal was a warning-level no_renegotiation alert. */ + ExpectIntEQ(wolfSSL_get_alert_history(ssl_s, &history), WOLFSSL_SUCCESS); + ExpectIntEQ(history.last_tx.level, alert_warning); + ExpectIntEQ(history.last_tx.code, no_renegotiation); + + /* The connection is still active and passes data: the server sends + * application data which the client receives and decrypts correctly, even + * though the client's renegotiation attempt was refused. The client + * surfaces the data once it has processed the no_renegotiation warning. */ + ExpectIntEQ(wolfSSL_write(ssl_s, "hello", 5), 5); + for (i = 0; i < 10 && ret != 5; i++) + ret = wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)); + ExpectIntEQ(ret, 5); + ExpectIntEQ(XMEMCMP(readBuf, "hello", 5), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + return EXPECT_RESULT(); +} + +/* F-4144: WOLFSSL_OP_NO_RENEGOTIATION on the client must refuse a + * server-initiated renegotiation (HelloRequest) with a no_renegotiation + * *warning* while keeping the established connection alive, rather than + * starting a secure renegotiation. */ +int test_helloRequest_no_renegotiation_option(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) + 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_ALERT_HISTORY history; + byte readBuf[16]; + int ret = WC_NO_ERR_TRACE(WOLFSSL_FATAL_ERROR); + int i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + XMEMSET(&history, 0, sizeof(history)); + 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); + + /* Client opts into rejecting peer-initiated renegotiation. */ + wolfSSL_set_options(ssl_c, WOLFSSL_OP_NO_RENEGOTIATION); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* Server asks the client to renegotiate by sending a HelloRequest, then + * waits for the ClientHello that never comes. */ + ExpectIntLT(wolfSSL_Rehandshake(ssl_s), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + + /* Client processes the HelloRequest. It must refuse without starting a + * renegotiation: the read returns WANT_READ (connection still alive). */ + ExpectIntLT(wolfSSL_read(ssl_c, readBuf, sizeof(readBuf)), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); + + /* The refusal was a warning-level no_renegotiation alert. */ + ExpectIntEQ(wolfSSL_get_alert_history(ssl_c, &history), WOLFSSL_SUCCESS); + ExpectIntEQ(history.last_tx.level, alert_warning); + ExpectIntEQ(history.last_tx.code, no_renegotiation); + + /* The connection is still active and passes data: the client sends + * application data which the server receives and decrypts correctly, even + * though its renegotiation request was refused. */ + ExpectIntEQ(wolfSSL_write(ssl_c, "hello", 5), 5); + for (i = 0; i < 10 && ret != 5; i++) + ret = wolfSSL_read(ssl_s, readBuf, sizeof(readBuf)); + ExpectIntEQ(ret, 5); + ExpectIntEQ(XMEMCMP(readBuf, "hello", 5), 0); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); +#endif + 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. */ diff --git a/tests/api/test_tls_ext.h b/tests/api/test_tls_ext.h index e9d07d81ecd..eda61aeb14c 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -27,6 +27,8 @@ 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_scr_no_renegotiation_option(void); +int test_helloRequest_no_renegotiation_option(void); int test_tls13_hrr_cipher_suite_mismatch(void); int test_tls13_ticket_age_out_of_window(void); int test_wolfSSL_DisableExtendedMasterSecret(void); From a773a884c8f28c3e940a193e188c607488ea2f6f Mon Sep 17 00:00:00 2001 From: Juliusz Sosinowicz Date: Wed, 3 Jun 2026 00:32:43 +0200 Subject: [PATCH 10/10] F-5810: require renegotiation_info on renegotiation ClientHello The server validated client_verify_data only inside TLSX_SecureRenegotiation_Parse, which never runs when the renegotiation_info extension is absent, so a renegotiation ClientHello that omitted it was never checked. Track a per-handshake renegInfoSeen flag and, after parsing the renegotiation ClientHello extensions, abort with handshake_failure if the extension was absent (RFC 5746 3.7). Also reject an SCSV received during renegotiation (RFC 5746 3.5). --- src/internal.c | 26 ++++++++++++++++++++++++++ src/tls.c | 3 +++ wolfssl/internal.h | 11 +++++++---- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/internal.c b/src/internal.c index 710a8a1c34a..f364188c09e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18769,6 +18769,8 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, ssl->options.acceptState = ACCEPT_FIRST_REPLY_DONE; ssl->options.handShakeState = NULL_STATE; ssl->secure_renegotiation->cache_status = SCR_CACHE_NEEDED; + /* Reset for the renegotiation_info presence check below. */ + ssl->secure_renegotiation->renegInfoSeen = 0; ret = InitHandshakeHashes(ssl); if (ret != 0) @@ -38659,6 +38661,17 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) 0) { TLSX* extension; +#ifdef HAVE_SECURE_RENEGOTIATION + /* SCSV not allowed on a renegotiation ClientHello (RFC 5746 3.5). */ + if (ssl->secure_renegotiation && + ssl->secure_renegotiation->enabled && + ssl->secure_renegotiation->verifySet) { + WOLFSSL_MSG("SCSV received on renegotiation ClientHello"); + SendAlert(ssl, alert_fatal, handshake_failure); + ret = SECURE_RENEGOTIATION_E; + goto out; + } +#endif /* check for TLS_EMPTY_RENEGOTIATION_INFO_SCSV suite */ ret = TLSX_AddEmptyRenegotiationInfo(&ssl->extensions, ssl->heap); if (ret != WOLFSSL_SUCCESS) { @@ -38889,6 +38902,19 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) *inOutIdx = begin + helloSz; /* skip extensions */ } +#ifdef HAVE_SECURE_RENEGOTIATION + /* renegotiation_info MUST be present on a renegotiation (RFC 5746 3.7). */ + if (ssl->secure_renegotiation && + ssl->secure_renegotiation->enabled && + ssl->secure_renegotiation->verifySet && + !ssl->secure_renegotiation->renegInfoSeen) { + WOLFSSL_MSG("Renegotiation ClientHello missing renegotiation_info"); + SendAlert(ssl, alert_fatal, handshake_failure); + ret = SECURE_RENEGOTIATION_E; + goto out; + } +#endif /* HAVE_SECURE_RENEGOTIATION */ + #ifdef WOLFSSL_DTLS_CID if (ssl->options.useDtlsCID) DtlsCIDOnExtensionsParsed(ssl); diff --git a/src/tls.c b/src/tls.c index 62118d0678b..48edf7d10e6 100644 --- a/src/tls.c +++ b/src/tls.c @@ -6142,6 +6142,9 @@ static int TLSX_SecureRenegotiation_Parse(WOLFSSL* ssl, const byte* input, if (ret == WOLFSSL_SUCCESS) ret = 0; } + /* renegotiation_info seen (checked by DoClientHello, RFC 5746 3.7) */ + if (ssl->secure_renegotiation != NULL) + ssl->secure_renegotiation->renegInfoSeen = 1; if (ret != 0 && ret != WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E)) { } else if (ssl->secure_renegotiation == NULL) { diff --git a/wolfssl/internal.h b/wolfssl/internal.h index fc918fbc770..a59fe21b318 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3493,13 +3493,16 @@ enum key_cache_state { /* Additional Connection State according to rfc5746 section 3.1 */ typedef struct SecureRenegotiation { - byte enabled; /* secure_renegotiation flag in rfc */ - byte verifySet; - byte startScr; /* server requested client to start scr */ + /* Single-bit flags grouped together so they pack into one storage unit. */ + WC_BITFIELD enabled:1; /* secure_renegotiation flag in rfc */ + WC_BITFIELD verifySet:1; + WC_BITFIELD startScr:1; /* server requested client to start scr */ + WC_BITFIELD renegInfoSeen:1; /* renegotiation_info ext seen this + * handshake (RFC 5746 3.7) */ + WC_BITFIELD subject_hash_set:1; /* if peer cert hash is set */ enum key_cache_state cache_status; /* track key cache state */ byte client_verify_data[TLS_FINISHED_SZ]; /* cached */ byte server_verify_data[TLS_FINISHED_SZ]; /* cached */ - byte subject_hash_set; /* if peer cert hash is set */ byte subject_hash[KEYID_SIZE]; /* peer cert hash */ Keys tmp_keys; /* can't overwrite real keys yet */ } SecureRenegotiation;