From a9aa459053658c04bededf207bcc8673809add96 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Thu, 23 Jul 2026 12:09:34 -0500 Subject: [PATCH 1/4] Enforce RFC 5746 renegotiation_info in TLS 1.2 client by default --- configure.ac | 21 ++++++ src/internal.c | 117 ++++++++++++++++++++++++----- src/ssl.c | 3 + src/ssl_api_ext.c | 49 +++++++++++- src/tls13.c | 11 +++ tests/api.c | 157 ++++++++++++++++++++++++--------------- tests/api/test_tls_ext.c | 72 ++++++++++++++++++ tests/api/test_tls_ext.h | 1 + wolfssl/internal.h | 17 ++++- wolfssl/ssl.h | 6 +- 10 files changed, 371 insertions(+), 83 deletions(-) diff --git a/configure.ac b/configure.ac index c2cda8369df..77367a73e94 100644 --- a/configure.ac +++ b/configure.ac @@ -8886,6 +8886,26 @@ AC_ARG_ENABLE([secure-renegotiation-info], [ ENABLED_SECURE_RENEGOTIATION_INFO=yes ] ) +# Legacy Server Connect +# By default a TLS 1.2 client requires the server to acknowledge the +# renegotiation_info extension on the initial handshake (RFC 5746 / RFC 9325). +# Enable this option to also connect to legacy servers that do not support +# secure renegotiation (not recommended). +AC_ARG_ENABLE([legacy-server-connect], + [AS_HELP_STRING([--enable-legacy-server-connect],[Allow connecting to servers that do not support secure renegotiation (default: disabled)])], + [ ENABLED_LEGACY_SERVER_CONNECT=$enableval ], + [ ENABLED_LEGACY_SERVER_CONNECT=no ] + ) + +if test "x$ENABLED_LEGACY_SERVER_CONNECT" = "xyes" +then + if test "x$ENABLED_HARDEN_TLS" != "xno" + then + AC_MSG_ERROR([--enable-legacy-server-connect conflicts with --enable-harden-tls (RFC 9325 3.5). Define WOLFSSL_HARDEN_TLS_NO_SCR_CHECK directly if this is intended.]) + fi + AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_ALLOW_LEGACY_SERVER_CONNECT" +fi + # Exporting Keying Material AC_ARG_ENABLE([keying-material], @@ -13613,6 +13633,7 @@ echo " * Session Ticket: $ENABLED_SESSION_TICKET" echo " * Extended Master Secret: $ENABLED_EXTENDED_MASTER" echo " * Renegotiation Indication: $ENABLED_RENEGOTIATION_INDICATION" echo " * Secure Renegotiation: $ENABLED_SECURE_RENEGOTIATION" +echo " * Legacy Server Connect: $ENABLED_LEGACY_SERVER_CONNECT" echo " * Keying Material Exporter: $ENABLED_KEYING_MATERIAL" echo " * All TLS Extensions: $ENABLED_TLSX" echo " * S/MIME: $ENABLED_SMIME" diff --git a/src/internal.c b/src/internal.c index 5914aa42033..2feeb61c73f 100644 --- a/src/internal.c +++ b/src/internal.c @@ -2699,6 +2699,17 @@ int InitSSL_Ctx(WOLFSSL_CTX* ctx, WOLFSSL_METHOD* method, void* heap) ctx->minMlDsaKeySz = MIN_MLDSAKEY_SZ; #endif /* WOLFSSL_HAVE_MLDSA */ ctx->verifyDepth = MAX_CHAIN_DEPTH; +#if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) && \ + !defined(WOLFSSL_ALLOW_LEGACY_SERVER_CONNECT) + /* RFC 5746 / RFC 9325: a TLS 1.2 client requires the server to acknowledge + * the renegotiation_info extension on the initial handshake. Enabled by + * default; define WOLFSSL_ALLOW_LEGACY_SERVER_CONNECT (or call + * wolfSSL_CTX_set_scr_check_enabled(ctx, 0)) to connect to servers that do + * not support secure renegotiation. */ + ctx->scr_check_enabled = 1; +#endif #ifdef OPENSSL_EXTRA ctx->cbioFlag = WOLFSSL_CBIO_NONE; #endif @@ -8023,6 +8034,58 @@ static void InitSSL_Multicast(WOLFSSL* ssl, WOLFSSL_CTX* ctx) } #endif /* WOLFSSL_MULTICAST */ +#if defined(HAVE_SECURE_RENEGOTIATION) || defined(HAVE_SERVER_RENEGOTIATION_INFO) +/* Set up the client's renegotiation_info advertising. Shared by InitSSL and the + * ClientHello send paths so the "advertise implies enforce" invariant survives + * WOLFSSL object reuse: without re-advertising after wolfSSL_clear the client + * would send no renegotiation_info yet still enforce its presence, failing + * every reused-object handshake. + * + * Willingness to perform a peer-initiated renegotiation is derived from + * ssl->ctx->useSecureReneg. A per-object wolfSSL_UseSecureRenegotiation() opt-in + * is not tracked here, so after wolfSSL_clear a reused object reverts to + * advertise-only; applications that renegotiate on reused objects should opt in + * at the context level with wolfSSL_CTX_UseSecureRenegotiation(). */ +int SetupClientSecureRenegotiation(WOLFSSL* ssl) +{ + int ret = WOLFSSL_SUCCESS; + + if (ssl->options.side == WOLFSSL_CLIENT_END) { + int useSecureReneg = ssl->ctx->useSecureReneg; + #ifdef HAVE_SECURE_RENEGOTIATION + int advertiseOnly = 0; + #endif + /* Advertise the empty renegotiation_info extension by default so the + * client can enforce RFC 5746 / RFC 9325 on the initial handshake. + * Advertising is always safe: legacy servers ignore it and secure + * servers echo it. */ + #if defined(WOLFSSL_SECURE_RENEGOTIATION_ON_BY_DEFAULT) + /* Integrator asked for secure renegotiation by default: keep the full + * renegotiation behavior (advertiseOnly stays 0). */ + useSecureReneg = 1; + #elif !defined(WOLFSSL_NO_TLS12) && defined(HAVE_SERVER_RENEGOTIATION_INFO) + #ifdef HAVE_SECURE_RENEGOTIATION + /* Advertising was forced on for the RFC 5746 check, not requested by + * the application, so do not also grant willingness to perform a + * peer-initiated renegotiation. */ + if (!useSecureReneg) + advertiseOnly = 1; + #endif + useSecureReneg = 1; + #endif + if (useSecureReneg) { + ret = wolfSSL_UseSecureRenegotiation(ssl); + #ifdef HAVE_SECURE_RENEGOTIATION + if (ret == WOLFSSL_SUCCESS && ssl->secure_renegotiation != NULL) + ssl->secure_renegotiation->advertiseOnly = (byte)advertiseOnly; + #endif + } + } + + return ret; +} +#endif /* HAVE_SECURE_RENEGOTIATION || HAVE_SERVER_RENEGOTIATION_INFO */ + int InitSSL(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup) { int ret; @@ -8239,8 +8302,10 @@ int InitSSL(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup) ssl->disabledCurves = ctx->disabledCurves; #endif #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) - ssl->scr_check_enabled = 1; + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + /* Inherit the renegotiation_info enforcement setting from the context. */ + ssl->scr_check_enabled = ctx->scr_check_enabled; #endif InitCiphers(ssl); @@ -8366,20 +8431,9 @@ int InitSSL(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup) #if defined(HAVE_SECURE_RENEGOTIATION) || \ defined(HAVE_SERVER_RENEGOTIATION_INFO) - if (ssl->options.side == WOLFSSL_CLIENT_END) { - int useSecureReneg = ssl->ctx->useSecureReneg; - /* use secure renegotiation by default (not recommend) */ - #if defined(WOLFSSL_SECURE_RENEGOTIATION_ON_BY_DEFAULT) || \ - (defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_NO_TLS12) && \ - !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK)) - useSecureReneg = 1; - #endif - if (useSecureReneg) { - ret = wolfSSL_UseSecureRenegotiation(ssl); - if (ret != WOLFSSL_SUCCESS) - return ret; - } - } + ret = SetupClientSecureRenegotiation(ssl); + if (ret != WOLFSSL_SUCCESS) + return ret; #endif /* HAVE_SECURE_RENEGOTIATION */ #ifdef WOLFSSL_QUIC @@ -9428,7 +9482,10 @@ void FreeHandshakeResources(WOLFSSL* ssl) #endif #ifdef HAVE_SECURE_RENEGOTIATION - if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) { + /* advertiseOnly clients never renegotiate, so do not retain (and leave + * unzeroed) the master secret and other handshake resources for them. */ + if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled && + !ssl->secure_renegotiation->advertiseOnly) { WOLFSSL_MSG("Secure Renegotiation needs to retain handshake resources"); return; } @@ -18573,8 +18630,13 @@ static int DoHelloRequest(WOLFSSL* ssl, word32 size) return FATAL_ERROR; } #ifdef HAVE_SECURE_RENEGOTIATION - else if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled) { - /* WOLFSSL_OP_NO_RENEGOTIATION: caller opted into rejecting + else if (ssl->secure_renegotiation && ssl->secure_renegotiation->enabled && + !ssl->secure_renegotiation->advertiseOnly) { + /* advertiseOnly: renegotiation_info was advertised only for the RFC + * 5746 initial-handshake check, so the application did not opt into + * secure renegotiation; the else branch below refuses the request. + * + * 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) { @@ -32654,6 +32716,17 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) return BAD_FUNC_ARG; } +#if defined(HAVE_SECURE_RENEGOTIATION) || defined(HAVE_SERVER_RENEGOTIATION_INFO) + /* Re-establish renegotiation_info advertising for a reused object + * (wolfSSL_clear frees it). Only when absent, so an in-progress + * renegotiation keeps its verify_data state. */ + if (ssl->secure_renegotiation == NULL) { + ret = SetupClientSecureRenegotiation(ssl); + if (ret != WOLFSSL_SUCCESS) + return ret; + } +#endif + #ifdef WOLFSSL_TLS13 if (IsAtLeastTLSv1_3(ssl->version)) return SendTls13ClientHello(ssl); @@ -33346,7 +33419,9 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) } #endif /* HAVE_TLS_EXTENSIONS */ -#if defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) +#if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) if (ssl->scr_check_enabled && (ssl->secure_renegotiation == NULL || !ssl->secure_renegotiation->enabled)) { /* If the server does not acknowledge the extension, the client @@ -33354,6 +33429,8 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) * terminating the connection. * https://www.rfc-editor.org/rfc/rfc9325#name-renegotiation-in-tls-12 */ WOLFSSL_MSG("ServerHello did not contain SCR extension"); + WOLFSSL_ERROR_VERBOSE(SECURE_RENEGOTIATION_E); + (void)SendAlert(ssl, alert_fatal, handshake_failure); return SECURE_RENEGOTIATION_E; } #endif diff --git a/src/ssl.c b/src/ssl.c index 47d731221f4..f74586a36a1 100644 --- a/src/ssl.c +++ b/src/ssl.c @@ -5667,6 +5667,9 @@ size_t wolfSSL_get_client_random(const WOLFSSL* ssl, unsigned char* out, #if defined(HAVE_SECURE_RENEGOTIATION) \ || defined(HAVE_SERVER_RENEGOTIATION_INFO) ssl->secure_renegotiation = NULL; + /* The renegotiation_info advertising is re-established when the next + * ClientHello is built (SendClientHello), so a reused client object + * keeps the "advertise implies enforce" invariant (RFC 5746). */ #endif #endif diff --git a/src/ssl_api_ext.c b/src/ssl_api_ext.c index 2e4a8952f10..7b63860f9f0 100644 --- a/src/ssl_api_ext.c +++ b/src/ssl_api_ext.c @@ -804,6 +804,13 @@ static int _Rehandshake(WOLFSSL* ssl) return SECURE_RENEGOTIATION_E; } + if (ssl->secure_renegotiation->advertiseOnly) { + /* Extension was advertised only for the RFC 5746 check; the + * application did not call wolfSSL_UseSecureRenegotiation(). */ + WOLFSSL_MSG("Secure Renegotiation not forced on by user"); + return SECURE_RENEGOTIATION_E; + } + #ifdef WOLFSSL_DTLS if (ssl->options.dtls && ssl->keys.dtls_epoch == 0xFFFF) { WOLFSSL_MSG("Secure Renegotiation not allowed. Epoch would wrap"); @@ -962,7 +969,8 @@ long wolfSSL_SSL_get_secure_renegotiation_support(WOLFSSL* ssl) #endif /* HAVE_SECURE_RENEGOTIATION_INFO */ #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) /* Get whether the secure renegotiation check is enabled for the object. * * @param [in] ssl SSL/TLS object. @@ -996,6 +1004,45 @@ WOLFSSL_API int wolfSSL_set_scr_check_enabled(WOLFSSL* ssl, byte enabled) ssl->scr_check_enabled = !!enabled; return WOLFSSL_SUCCESS; } + +/* Get whether the secure renegotiation check is enabled for the context. + * + * @param [in] ctx SSL/TLS context object. + * @return Non-zero when the check is enabled, 0 otherwise. + * @return BAD_FUNC_ARG when ctx is NULL. + */ +WOLFSSL_API int wolfSSL_CTX_get_scr_check_enabled(const WOLFSSL_CTX* ctx) +{ + WOLFSSL_ENTER("wolfSSL_CTX_get_scr_check_enabled"); + + if (ctx == NULL) + return BAD_FUNC_ARG; + + return ctx->scr_check_enabled; +} + +/* Set whether the secure renegotiation check is enabled for the context. + * + * WOLFSSL objects created from the context inherit this setting. Disabling the + * check allows connecting to servers that do not support secure renegotiation + * (RFC 5746), which is not recommended. + * + * @param [in] ctx SSL/TLS context object. + * @param [in] enabled Non-zero to enable the check, 0 to disable it. + * @return WOLFSSL_SUCCESS on success. + * @return BAD_FUNC_ARG when ctx is NULL. + */ +WOLFSSL_API int wolfSSL_CTX_set_scr_check_enabled(WOLFSSL_CTX* ctx, + byte enabled) +{ + WOLFSSL_ENTER("wolfSSL_CTX_set_scr_check_enabled"); + + if (ctx == NULL) + return BAD_FUNC_ARG; + + ctx->scr_check_enabled = !!enabled; + return WOLFSSL_SUCCESS; +} #endif #if defined(HAVE_SESSION_TICKET) diff --git a/src/tls13.c b/src/tls13.c index 24b15bea2dc..4d723b24685 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4647,6 +4647,17 @@ int SendTls13ClientHello(WOLFSSL* ssl) return BAD_FUNC_ARG; } +#if defined(HAVE_SECURE_RENEGOTIATION) || defined(HAVE_SERVER_RENEGOTIATION_INFO) + /* Re-establish renegotiation_info advertising for a reused object + * (wolfSSL_clear frees it) so a TLS 1.2 downgrade still enforces what the + * ClientHello advertised. Only when absent, to keep any existing state. */ + if (ssl->secure_renegotiation == NULL) { + ret = SetupClientSecureRenegotiation(ssl); + if (ret != WOLFSSL_SUCCESS) + return ret; + } +#endif + ssl->options.buildingMsg = 1; major = SSLv3_MAJOR; tls12minor = TLSv1_2_MINOR; diff --git a/tests/api.c b/tests/api.c index d8cea969a14..6d4716eb778 100644 --- a/tests/api.c +++ b/tests/api.c @@ -9901,35 +9901,48 @@ static int test_wolfSSL_SCR_Reconnect(void) /* Test SCR check when server doesn't reply to secure_renegotiation. */ #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) && \ - defined(HAVE_SECURE_RENEGOTIATION) && \ + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) && \ + !defined(WOLFSSL_ALLOW_LEGACY_SERVER_CONNECT) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) -/* IO callback to remove secure renegotiation extension from ServerHello */ -static int test_SCR_check_remove_ext_io_cb(WOLFSSL *ssl, char *buf, int sz, void *ctx) -{ - static int sentServerHello = FALSE; - - if (!sentServerHello) { - /* Look for secure renegotiation extension: 0xFF 0x01 (extension type) */ - byte renegExt[] = { 0xFF, 0x01 }; - size_t i; - - if (sz < (int)sizeof(renegExt)) - return test_memio_write_cb(ssl, buf, sz, ctx); +/* Set to 1 to strip renegotiation_info from the next ServerHello sent. */ +static int test_scr_strip_serverhello = 0; - /* Search for the extension in the buffer */ - for (i = 0; i < (size_t)sz - sizeof(renegExt); i++) { - if (XMEMCMP(buf + i, renegExt, sizeof(renegExt)) == 0) { - /* Found the extension. Remove it by changing the type to something - * unrecognized so it won't be parsed as secure renegotiation. */ - buf[i+1] = 0x11; - break; +/* IO callback to remove secure renegotiation extension from ServerHello. + * Anchors to the ServerHello extension block so a chance 0xFF01 in the server + * random or session id cannot be mistaken for the renegotiation_info type. */ +static int test_SCR_check_remove_ext_io_cb(WOLFSSL *ssl, char *buf, int sz, + void *ctx) +{ + int off; + int sidLen; + int extLen; + int extEnd; + + if (test_scr_strip_serverhello && sz > 44 && + (byte)buf[0] == 0x16 && (byte)buf[5] == 0x02) { + /* 5 record hdr + 4 handshake hdr + 2 version + 32 random */ + off = 5 + 4 + 2 + 32; + sidLen = (byte)buf[off]; + off += 1 + sidLen + 2 + 1; /* session id + cipher suite + compression */ + if (off + 2 <= sz) { + extLen = ((byte)buf[off] << 8) | (byte)buf[off + 1]; + off += 2; + extEnd = off + extLen; + if (extEnd > sz) + extEnd = sz; + for (; off + 1 < extEnd; off++) { + if ((byte)buf[off] == 0xFF && (byte)buf[off + 1] == 0x01) { + /* Change the type so the client does not parse it as + * renegotiation_info, simulating a server that omits it. */ + buf[off + 1] = 0x11; + test_scr_strip_serverhello = 0; /* only the ServerHello */ + break; + } } } - sentServerHello = TRUE; } - /* Call the original test_memio_write_cb */ return test_memio_write_cb(ssl, buf, sz, ctx); } #endif @@ -9938,64 +9951,57 @@ static int test_wolfSSL_SCR_check_enabled(void) { EXPECT_DECLS; #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) && \ - defined(HAVE_SECURE_RENEGOTIATION) && \ + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) && \ + !defined(WOLFSSL_ALLOW_LEGACY_SERVER_CONNECT) && \ 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; + WOLFSSL_ALERT_HISTORY history; int ret; - int enabled; + /* Phase 1: a default TLS 1.2 client advertises renegotiation_info and, + * when the server acknowledges it, the handshake succeeds and secure + * renegotiation is negotiated. Enforcement is on by default. */ XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - - /* Set up client and server */ ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, wolfTLSv1_2_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(1, wolfSSL_CTX_get_scr_check_enabled(ctx_c)); + ExpectIntEQ(1, wolfSSL_get_scr_check_enabled(ssl_c)); + ExpectIntEQ(0, test_memio_do_handshake(ssl_c, ssl_s, 10, NULL)); + ExpectIntNE(0, (int)wolfSSL_SSL_get_secure_renegotiation_support(ssl_c)); - /* Enable secure renegotiation on client (so it sends the extension) */ - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSecureRenegotiation(ctx_c)); - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_UseSecureRenegotiation(ssl_c)); - - /* Set up IO callback on server to remove the extension from ServerHello */ - wolfSSL_SSLSetIOSend(ssl_s, test_SCR_check_remove_ext_io_cb); - - /* Try to connect - should fail with SECURE_RENEGOTIATION_E */ - ret = test_memio_do_handshake(ssl_c, ssl_s, 10, NULL); - ExpectIntNE(0, ret); /* Handshake should fail */ - ret = wolfSSL_get_error(ssl_c, 0); - ExpectIntEQ(WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E), ret); - - /* Clean up for next attempt */ wolfSSL_free(ssl_c); ssl_c = NULL; wolfSSL_free(ssl_s); ssl_s = NULL; - test_memio_clear_buffer(&test_ctx, 1); - test_memio_clear_buffer(&test_ctx, 0); + wolfSSL_CTX_free(ctx_c); + ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); + ctx_s = NULL; - /* Set up new client and server */ + /* Phase 2: a default client aborts with a fatal handshake_failure alert + * when the server's ServerHello omits renegotiation_info (RFC 5746 4.1 / + * RFC 9325). No explicit opt-in is required. */ + 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); - - /* Enable secure renegotiation on client */ - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_UseSecureRenegotiation(ctx_c)); - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_UseSecureRenegotiation(ssl_c)); - - /* Set up IO callback on server to remove the extension from ServerHello */ wolfSSL_SSLSetIOSend(ssl_s, test_SCR_check_remove_ext_io_cb); + test_scr_strip_serverhello = 1; - /* Disable the SCR check */ - ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_set_scr_check_enabled(ssl_c, 0)); - - /* Verify the state is 0 */ - enabled = wolfSSL_get_scr_check_enabled(ssl_c); - ExpectIntEQ(0, enabled); + ret = test_memio_do_handshake(ssl_c, ssl_s, 10, NULL); + ExpectIntNE(0, ret); + ExpectIntEQ(WC_NO_ERR_TRACE(SECURE_RENEGOTIATION_E), + wolfSSL_get_error(ssl_c, ret)); + /* The callback clears this only if it actually stripped the extension. */ + ExpectIntEQ(0, test_scr_strip_serverhello); - /* Now connection should succeed */ - ExpectIntEQ(0, test_memio_do_handshake(ssl_c, ssl_s, 10, NULL)); + XMEMSET(&history, 0, sizeof(history)); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_get_alert_history(ssl_c, &history)); + ExpectIntEQ(alert_fatal, history.last_tx.level); + ExpectIntEQ(handshake_failure, history.last_tx.code); - /* Cleanup */ wolfSSL_free(ssl_c); ssl_c = NULL; wolfSSL_free(ssl_s); @@ -10004,6 +10010,36 @@ static int test_wolfSSL_SCR_check_enabled(void) ctx_c = NULL; wolfSSL_CTX_free(ctx_s); ctx_s = NULL; + test_scr_strip_serverhello = 0; + + /* Phase 3: the enforcement setting can be toggled per object and per + * context, and WOLFSSL objects inherit the context setting. */ + ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_2_client_method())); + ExpectIntEQ(1, wolfSSL_CTX_get_scr_check_enabled(ctx_c)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectIntEQ(1, wolfSSL_get_scr_check_enabled(ssl_c)); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_set_scr_check_enabled(ssl_c, 0)); + ExpectIntEQ(0, wolfSSL_get_scr_check_enabled(ssl_c)); + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_set_scr_check_enabled(ssl_c, 1)); + ExpectIntEQ(1, wolfSSL_get_scr_check_enabled(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + + /* Disabling on the context propagates to newly created objects. */ + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_set_scr_check_enabled(ctx_c, 0)); + ExpectIntEQ(0, wolfSSL_CTX_get_scr_check_enabled(ctx_c)); + ExpectNotNull(ssl_c = wolfSSL_new(ctx_c)); + ExpectIntEQ(0, wolfSSL_get_scr_check_enabled(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_CTX_free(ctx_c); + ctx_c = NULL; + + /* NULL argument handling. */ + ExpectIntEQ(BAD_FUNC_ARG, wolfSSL_get_scr_check_enabled(NULL)); + ExpectIntEQ(BAD_FUNC_ARG, wolfSSL_set_scr_check_enabled(NULL, 1)); + ExpectIntEQ(BAD_FUNC_ARG, wolfSSL_CTX_get_scr_check_enabled(NULL)); + ExpectIntEQ(BAD_FUNC_ARG, wolfSSL_CTX_set_scr_check_enabled(NULL, 1)); #endif return EXPECT_RESULT(); } @@ -38225,6 +38261,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_scr_verify_data_mismatch), TEST_DECL(test_scr_no_renegotiation_option), TEST_DECL(test_helloRequest_no_renegotiation_option), + TEST_DECL(test_helloRequest_advertise_only_refused), 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 34d95f12125..7ce2bb81b12 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -649,6 +649,78 @@ int test_helloRequest_no_renegotiation_option(void) return EXPECT_RESULT(); } +/* A client that advertises renegotiation_info only for the RFC 5746 initial + * handshake check (the default, with no wolfSSL_UseSecureRenegotiation() opt-in) + * must still refuse a server-initiated renegotiation rather than perform one, + * even without WOLFSSL_OP_NO_RENEGOTIATION. */ +int test_helloRequest_advertise_only_refused(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); + /* Server opts into secure renegotiation so it can send a HelloRequest. The + * client does NOT opt in: it only advertises renegotiation_info by default + * for the RFC 5746 check. */ + ExpectIntEQ(wolfSSL_CTX_UseSecureRenegotiation(ctx_s), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSecureRenegotiation(ssl_s), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + /* The client negotiated renegotiation_info (the RFC 5746 check passed) but + * must not be willing to renegotiate on the server's request. */ + ExpectIntNE(0, (int)wolfSSL_SSL_get_secure_renegotiation_support(ssl_c)); + + /* advertiseOnly clients release handshake resources (master secret, + * arrays) after the handshake rather than retaining them for a + * renegotiation that will never happen. */ + ExpectNull(ssl_c->arrays); + + /* Server asks the client to renegotiate, then waits for a ClientHello. */ + ExpectIntLT(wolfSSL_Rehandshake(ssl_s), 0); + ExpectIntEQ(wolfSSL_get_error(ssl_s, -1), WOLFSSL_ERROR_WANT_READ); + + /* Client processes the HelloRequest and refuses without renegotiating. */ + 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. */ + 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 90ccdf17c96..3d2038be302 100644 --- a/tests/api/test_tls_ext.h +++ b/tests/api/test_tls_ext.h @@ -30,6 +30,7 @@ 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_helloRequest_advertise_only_refused(void); int test_tls13_hrr_cipher_suite_mismatch(void); int test_tls13_ticket_age_out_of_window(void); int test_wolfSSL_DisableExtendedMasterSecret(void); diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 38e5015d0d8..34b61acc7f2 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3558,6 +3558,11 @@ typedef struct SecureRenegotiation { 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 */ +#ifdef HAVE_SECURE_RENEGOTIATION + WC_BITFIELD advertiseOnly:1; /* extension advertised for the RFC + * 5746 initial-handshake check only; + * refuse peer-initiated renegotiation */ +#endif 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 */ @@ -3571,6 +3576,8 @@ WOLFSSL_LOCAL int TLSX_UseSecureRenegotiation(TLSX** extensions, void* heap); WOLFSSL_LOCAL int TLSX_AddEmptyRenegotiationInfo(TLSX** extensions, void* heap); #endif +WOLFSSL_LOCAL int SetupClientSecureRenegotiation(WOLFSSL* ssl); + #endif /* HAVE_SECURE_RENEGOTIATION */ #ifdef HAVE_SESSION_TICKET @@ -4095,6 +4102,13 @@ struct WOLFSSL_CTX { #if defined(HAVE_SECURE_RENEGOTIATION) || defined(HAVE_SERVER_RENEGOTIATION_INFO) byte useSecureReneg:1; /* when set will set WOLFSSL objects generated to enable */ #endif +#if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + byte scr_check_enabled:1; /* require server renegotiation_info on the + * initial handshake (RFC 5746/9325); + * inherited by WOLFSSL objects */ +#endif #ifdef HAVE_ENCRYPT_THEN_MAC byte disallowEncThenMac:1; /* Don't do Encrypt-Then-MAC */ #endif @@ -6702,7 +6716,8 @@ struct WOLFSSL { int secLevel; /* The security level of system-wide crypto policy. */ #endif /* WOLFSSL_SYS_CRYPTO_POLICY */ #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) WC_BITFIELD scr_check_enabled:1; /* enable/disable SCR check */ #endif #ifdef HAVE_WRITE_DUP diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index b71810d3181..829cd1089ae 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -4963,9 +4963,13 @@ WOLFSSL_API int wolfSSL_SecureResume(WOLFSSL* ssl); WOLFSSL_API long wolfSSL_SSL_get_secure_renegotiation_support(WOLFSSL* ssl); #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ - defined(WOLFSSL_HARDEN_TLS) && !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) WOLFSSL_API int wolfSSL_get_scr_check_enabled(const WOLFSSL* ssl); WOLFSSL_API int wolfSSL_set_scr_check_enabled(WOLFSSL* ssl, byte enabled); +WOLFSSL_API int wolfSSL_CTX_get_scr_check_enabled(const WOLFSSL_CTX* ctx); +WOLFSSL_API int wolfSSL_CTX_set_scr_check_enabled(WOLFSSL_CTX* ctx, + byte enabled); #endif #endif From be7a0aff115766b38a6345a713dbd386a25eb2db Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 24 Jul 2026 10:50:16 -0500 Subject: [PATCH 2/4] Fix dtls test --- tests/api/test_dtls.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/api/test_dtls.c b/tests/api/test_dtls.c index aa0c9578be2..61f31441ed0 100644 --- a/tests/api/test_dtls.c +++ b/tests/api/test_dtls.c @@ -2417,6 +2417,14 @@ int test_dtls_certreq_order(void) ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, NULL, &ssl_c, NULL, wolfDTLSv1_2_client_method, NULL), 0); +#if defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ + !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) + /* This fixed capture predates renegotiation_info advertising, so its + * ServerHello omits the extension. Disable the RFC 5746 check so the test + * exercises message ordering rather than renegotiation_info enforcement. */ + ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_set_scr_check_enabled(ssl_c, 0)); +#endif + /* start handshake, send first ClientHello */ ExpectIntEQ(wolfSSL_connect(ssl_c), -1); ExpectIntEQ(wolfSSL_get_error(ssl_c, -1), WOLFSSL_ERROR_WANT_READ); From 13839801d7711ae2b51f9832bf8940087c655689 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 24 Jul 2026 11:50:57 -0500 Subject: [PATCH 3/4] Fix tests --- tests/api/test_tls_ext.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/api/test_tls_ext.c b/tests/api/test_tls_ext.c index 7ce2bb81b12..b5e02dbc0af 100644 --- a/tests/api/test_tls_ext.c +++ b/tests/api/test_tls_ext.c @@ -688,10 +688,13 @@ int test_helloRequest_advertise_only_refused(void) * must not be willing to renegotiate on the server's request. */ ExpectIntNE(0, (int)wolfSSL_SSL_get_secure_renegotiation_support(ssl_c)); - /* advertiseOnly clients release handshake resources (master secret, - * arrays) after the handshake rather than retaining them for a - * renegotiation that will never happen. */ - ExpectNull(ssl_c->arrays); + /* advertiseOnly clients do not retain handshake resources for a + * renegotiation that will never happen. Unless the build deliberately + * keeps the arrays for another reason (saveArrays, e.g. keying material), + * the master secret and arrays are released after the handshake. */ + if (ssl_c != NULL && ssl_c->options.saveArrays == 0) { + ExpectNull(ssl_c->arrays); + } /* Server asks the client to renegotiate, then waits for a ClientHello. */ ExpectIntLT(wolfSSL_Rehandshake(ssl_s), 0); From d4533b63ae0acb83247d0d7b84bf4f5ba0009729 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Fri, 24 Jul 2026 15:11:13 -0500 Subject: [PATCH 4/4] Fix tests --- src/internal.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index 2feeb61c73f..6616975e382 100644 --- a/src/internal.c +++ b/src/internal.c @@ -33422,8 +33422,13 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) #if !defined(NO_WOLFSSL_CLIENT) && !defined(WOLFSSL_NO_TLS12) && \ defined(HAVE_SERVER_RENEGOTIATION_INFO) && \ !defined(WOLFSSL_HARDEN_TLS_NO_SCR_CHECK) - if (ssl->scr_check_enabled && (ssl->secure_renegotiation == NULL || - !ssl->secure_renegotiation->enabled)) { + /* SSL 3.0 does not carry the renegotiation_info extension in its + * ServerHello (RFC 5746 relies on the SCSV for SSL 3.0), so only + * enforce for TLS 1.0+ and DTLS. ssl->options.tls is not yet set at + * this point, so key off the negotiated version. */ + if (ssl->version.minor != SSLv3_MINOR && ssl->scr_check_enabled && + (ssl->secure_renegotiation == NULL || + !ssl->secure_renegotiation->enabled)) { /* If the server does not acknowledge the extension, the client * MUST generate a fatal handshake_failure alert prior to * terminating the connection.