From 740acd78636e31ef29e847d7d42df03bcfd63bc4 Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Thu, 22 Oct 2026 16:58:22 -0600 Subject: [PATCH 1/2] harden downgrade path for ECH --- src/internal.c | 48 +++++++- src/ssl_api_hs.c | 12 ++ src/tls.c | 8 ++ src/tls13.c | 13 ++- tests/api.c | 298 +++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 368 insertions(+), 11 deletions(-) diff --git a/src/internal.c b/src/internal.c index 65f1ebe2457..3877fc1f022 100644 --- a/src/internal.c +++ b/src/internal.c @@ -20681,7 +20681,14 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, /* Drop the unverified ticket (SetTicket() made the session * unique, so this cannot clear one shared with the app). */ if (ssl->options.side == WOLFSSL_CLIENT_END && - ssl->msgsReceived.got_session_ticket) { + ssl->msgsReceived.got_session_ticket + #ifdef HAVE_ECH + /* no ticket was stored when ECH is rejected */ + && !(ssl->echConfigs != NULL && + !ssl->options.disableECH && + !ssl->options.echAccepted) + #endif + ) { ForceZero(ssl->session->ticket, ssl->session->ticketLen); if (ssl->session->ticketLenAlloc > 0) { XFREE(ssl->session->ticket, ssl->heap, @@ -20739,7 +20746,13 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, || (ssl->msgsReceived.got_session_ticket && ssl->session->ticketLen > 0) #endif - )) { + ) +#ifdef HAVE_ECH + /* ignore the session when ECH is rejected */ + && !(ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) +#endif + ) { SetupSession(ssl); #ifndef NO_SESSION_CACHE AddSession(ssl); @@ -36808,7 +36821,14 @@ static int IsTls13CipherSuite(byte first, byte second) return BUFFER_ERROR; } - if (PickHashSigAlgo(ssl, input + *inOutIdx, len, 0) != 0 && + if ( + #ifdef HAVE_ECH + /* no client cert is sent when ECH is rejected, so an error with + * picking the alg would reveal information about the cert */ + !(ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) && + #endif + PickHashSigAlgo(ssl, input + *inOutIdx, len, 0) != 0 && ssl->buffers.certificate && ssl->buffers.certificate->buffer) { #ifdef HAVE_PK_CALLBACKS @@ -36912,6 +36932,18 @@ static int IsTls13CipherSuite(byte first, byte second) len -= (word16)(OPAQUE16_LEN) + dnSz; } + #if defined(HAVE_ECH) + /* RFC 9849 s6.1.7: ECH was offered but rejected by the server... + * the client MUST respond with an empty Certificate message. */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + ssl->options.sendVerify = SEND_BLANK_CERT; + WOLFSSL_LEAVE("DoCertificateRequest", 0); + WOLFSSL_END(WC_FUNC_CERTIFICATE_REQUEST_DO); + return 0; + } + #endif + #ifdef WOLFSSL_CERT_SETUP_CB #ifdef OPENSSL_EXTRA /* call client cert callback if no cert has been loaded */ @@ -40008,6 +40040,16 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, return SESSION_TICKET_EXPECT_E; } +#ifdef HAVE_ECH + /* ignore session ticket when ECH is rejected */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + ssl->expect_session_ticket = 0; + *inOutIdx += size; + return 0; + } +#endif + /* A renewed ticket while resuming confirms resumption; check before the * SetupSession() in DoFinished refreshes the cached suite/EMS and masks a * downgrade. diff --git a/src/ssl_api_hs.c b/src/ssl_api_hs.c index 2f28ed44de0..ec8e4c8ba9a 100644 --- a/src/ssl_api_hs.c +++ b/src/ssl_api_hs.c @@ -689,6 +689,18 @@ int wolfSSL_connect(WOLFSSL* ssl) } } + #if defined(HAVE_ECH) + /* RFC 9849 s6.1.4: negotiating below TLS 1.3 means the server rejected + * ECH. Abort with ech_required instead of reporting success. */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + SendAlert(ssl, alert_fatal, ech_required); + ssl->error = ECH_REQUIRED_E; + WOLFSSL_ERROR_VERBOSE(ECH_REQUIRED_E); + return WOLFSSL_FATAL_ERROR; + } + #endif /* HAVE_ECH */ + ssl->options.connectState = SECOND_REPLY_DONE; WOLFSSL_MSG("connect state: SECOND_REPLY_DONE"); FALL_THROUGH; diff --git a/src/tls.c b/src/tls.c index a0acf532869..e3799029953 100644 --- a/src/tls.c +++ b/src/tls.c @@ -17177,6 +17177,14 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) TLSX_SetResponse(ssl, TLSX_ECH); } } + else if (!isServer && ssl->echConfigs != NULL && + !ssl->options.disableECH) { + /* An ECH client has configs but is instead setup to use TLS1.2 or + * lower, this is incompatible with ECH so fail instead */ + WOLFSSL_MSG("ECH configs set but TLS 1.3 not negotiable"); + WOLFSSL_ERROR_VERBOSE(BAD_STATE_E); + ret = BAD_STATE_E; + } #endif #endif diff --git a/src/tls13.c b/src/tls13.c index 451da4cb4cd..50ef0527136 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6732,7 +6732,13 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, #endif #ifdef WOLFSSL_CERT_SETUP_CB - if ((ret = CertSetupCbWrapper(ssl)) != 0) + if ( + #ifdef HAVE_ECH + /* no client cert is sent when ECH is rejected */ + !(ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) && + #endif + (ret = CertSetupCbWrapper(ssl)) != 0) return ret; #endif @@ -10801,6 +10807,11 @@ static int SendTls13Certificate(WOLFSSL* ssl) #if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) /* call client cert callback if no cert has been loaded */ if ((ssl->ctx->CBClientCert != NULL) && + #ifdef HAVE_ECH + /* no client cert is sent when ECH is rejected */ + !(ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) && + #endif (!ssl->buffers.certificate || !ssl->buffers.certificate->buffer)) { ret = ssl->ctx->CBClientCert(ssl, &x509, &pkey); if (ret == 1) { diff --git a/tests/api.c b/tests/api.c index 6ca1a7cd0ec..8f54895d4de 100644 --- a/tests/api.c +++ b/tests/api.c @@ -17931,6 +17931,16 @@ static int test_ech_client_ssl_ready(WOLFSSL* ssl) return TEST_SUCCESS; } +#ifdef WOLFSSL_CERT_SETUP_CB +/* Client cert setup callback: counts calls and fails the handshake */ +static int test_ech_cert_setup_cb(WOLFSSL* ssl, void* arg) +{ + (void)ssl; + (*(int*)arg)++; + return 0; +} +#endif + static int test_wolfSSL_Tls13_ECH_all_algos_ex(void) { EXPECT_DECLS; @@ -18956,7 +18966,7 @@ static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void) * ech config * the cert check should pass and the client aborts with ech_required */ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, - int validName) + int validName, method_provider serverMethod) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; @@ -18965,8 +18975,8 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, XMEMSET(&test_ctx, 0, sizeof(test_ctx)); - test_ctx.s_cb.method = wolfTLSv1_3_server_method; - test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.method = serverMethod; + test_ctx.c_cb.method = wolfSSLv23_client_method; ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); @@ -19029,14 +19039,217 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid(void) EXPECT_DECLS; /* "example.com" appears in the SAN of certs/server-cert.pem */ - ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("example.com", 1), - TEST_SUCCESS); - ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0), - TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("example.com", 1, + wolfTLSv1_3_server_method), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0, + wolfTLSv1_3_server_method), TEST_SUCCESS); +#ifndef WOLFSSL_NO_TLS12 + /* a downgrade to TLS 1.2 also rejects ECH, the good name is covered by + * test_wolfSSL_Tls13_ECH_tls12_downgrade */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0, + wolfTLSv1_2_server_method), TEST_SUCCESS); +#endif return EXPECT_RESULT(); } +/* RFC 9849 S6.1.4: If a server downgrades a connection to TLS 1.2 the + * ECH client treats that as rejection */ +static int test_wolfSSL_Tls13_ECH_tls12_downgrade(void) +{ + EXPECT_DECLS; +#ifndef WOLFSSL_NO_TLS12 + test_ssl_memio_ctx test_ctx; + byte echConfigs[ECH_CONFIG_LEN]; + word32 echConfigsLen = sizeof(echConfigs); + const char* publicName = "example.com"; +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + word32 ticketLen = 0; +#endif +#ifdef WOLFSSL_CERT_SETUP_CB + int certSetupCalls = 0; +#endif + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_2_server_method; + test_ctx.c_cb.method = wolfSSLv23_client_method; +#if defined(OPENSSL_EXTRA) && defined(HAVE_ECC) && defined(HAVE_ED25519) + /* client credential with no signature algorithm in the CertificateRequest + * must not fail the handshake before the empty Certificate is sent */ + test_ctx.c_cb.certPemFile = cliEdCertFile; + test_ctx.c_cb.keyPemFile = cliEdKeyFile; +#endif + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); +#if defined(OPENSSL_EXTRA) && defined(HAVE_ECC) && defined(HAVE_ED25519) + ExpectIntEQ(wolfSSL_set1_sigalgs_list(test_ctx.s_ssl, + "RSA+SHA256:ECDSA+SHA256"), WOLFSSL_SUCCESS); +#endif +#ifdef WOLFSSL_CERT_SETUP_CB + /* no certificate is selected when an empty Certificate is sent */ + wolfSSL_CTX_set_cert_cb(test_ctx.c_ctx, test_ech_cert_setup_cb, + &certSetupCalls); +#endif + + /* Generate the ECH configs on the server. Leave ECH enabled - since the + * server is TLS 1.2 it should not support ECH */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, publicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(test_ctx.s_ctx, echConfigs, + &echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echConfigs, + echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + "ech-private.com", (word16)XSTRLEN("ech-private.com")), + WOLFSSL_SUCCESS); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + ExpectIntEQ(wolfSSL_UseSessionTicket(test_ctx.c_ssl), WOLFSSL_SUCCESS); +#endif + + /* request a client cert, the client must respond with an empty one */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* set sni to the public name so it matches the presented cert, and so the + * client is not rejected based on SNI */ + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + publicName, (word16)XSTRLEN(publicName)), WOLFSSL_SUCCESS); + + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + /* client aborts after the handshake completed */ + ExpectIntEQ(test_ctx.c_ssl->options.serverState, SERVER_FINISHED_COMPLETE); + ExpectIntEQ(wolfSSL_is_init_finished(test_ctx.s_ssl), 1); + ExpectIntEQ(test_ctx.s_ssl->options.havePeerCert, 0); +#ifdef WOLFSSL_CERT_SETUP_CB + ExpectIntEQ(certSetupCalls, 0); +#endif + + /* session ID and ticket from the rejected connection are ignored */ + ExpectIntEQ(wolfSSL_SessionIsSetup(test_ctx.c_ssl->session), 0); +#if defined(HAVE_SESSION_TICKET) && !defined(WOLFSSL_NO_DEF_TICKET_ENC_CB) + ExpectIntEQ(test_ctx.c_ssl->msgsReceived.got_session_ticket, 1); + ExpectIntEQ(wolfSSL_get_SessionTicket(test_ctx.c_ssl, NULL, &ticketLen), + WC_NO_ERR_TRACE(LENGTH_ONLY_E)); + ExpectIntEQ(ticketLen, 0); +#endif + + test_ssl_memio_cleanup(&test_ctx); +#endif + return EXPECT_RESULT(); +} + +/* a client that cannot negotiate TLS 1.3 cannot offer ECH, so assert that it + * fails before sending any data */ +static int test_wolfSSL_Tls13_ECH_tls12_client(void) +{ + EXPECT_DECLS; +#ifndef WOLFSSL_NO_TLS12 + test_ssl_memio_ctx test_ctx; + byte echConfigs[ECH_CONFIG_LEN]; + word32 echConfigsLen = sizeof(echConfigs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfSSLv23_server_method; + test_ctx.c_cb.method = wolfTLSv1_2_client_method; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, "example.com", + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(test_ctx.s_ctx, echConfigs, + &echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echConfigs, + echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + "ech-private.com", (word16)XSTRLEN("ech-private.com")), + WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_connect(test_ctx.c_ssl), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(BAD_STATE_E)); + /* no ClientHello was sent */ + ExpectIntEQ(test_ctx.s_len, 0); + + test_ssl_memio_cleanup(&test_ctx); +#endif + return EXPECT_RESULT(); +} + +/* RFC 9849 s6.1: a client offering ECH MUST NOT offer to resume a TLS 1.2 or + * below session */ +static int test_wolfSSL_Tls13_ECH_tls12_resume(void) +{ + EXPECT_DECLS; +#if !defined(WOLFSSL_NO_TLS12) && !defined(NO_SESSION_CACHE) + 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_SESSION* sess = NULL; + + /* full TLS 1.2 handshake without ECH to get a session */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectNotNull(sess = wolfSSL_get1_session(ssl_c)); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + + /* ECH would be rejected: the TLS 1.2 session cannot be offered with ECH, so + * the client fails before sending a ClientHello */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfTLSv1_2_server_method), 0); + ExpectIntEQ(test_ech_server_ctx_ready(ctx_s), TEST_SUCCESS); + ExpectIntEQ(test_ech_server_ssl_ready(ssl_s), TEST_SUCCESS); + ExpectIntEQ(test_ech_client_ssl_ready(ssl_c), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_connect(ssl_c), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(BAD_STATE_E)); + ExpectIntEQ(test_ctx.s_len, 0); + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + wolfSSL_CTX_free(ctx_s); + ctx_s = NULL; + + /* ECH would be accepted: same failure, the server is never reached */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfSSLv23_server_method), 0); + ExpectIntEQ(test_ech_server_ctx_ready(ctx_s), TEST_SUCCESS); + ExpectIntEQ(test_ech_server_ssl_ready(ssl_s), TEST_SUCCESS); + ExpectIntEQ(test_ech_client_ssl_ready(ssl_c), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_set_session(ssl_c, sess), WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_connect(ssl_c), WOLFSSL_FATAL_ERROR); + ExpectIntEQ(wolfSSL_get_error(ssl_c, WOLFSSL_FATAL_ERROR), + WC_NO_ERR_TRACE(BAD_STATE_E)); + ExpectIntEQ(test_ctx.s_len, 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(); +} + /* when ECH is rejected and the server requests a client certificate the client * must respond with an empty cert */ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) @@ -19046,6 +19259,9 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) byte echConfigs[ECH_CONFIG_LEN]; word32 echConfigsLen = sizeof(echConfigs); const char* publicName = "example.com"; +#ifdef WOLFSSL_CERT_SETUP_CB + int certSetupCalls = 0; +#endif XMEMSET(&test_ctx, 0, sizeof(test_ctx)); @@ -19053,6 +19269,11 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) test_ctx.c_cb.method = wolfTLSv1_3_client_method; ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); +#ifdef WOLFSSL_CERT_SETUP_CB + /* no certificate is selected when an empty Certificate is always sent */ + wolfSSL_CTX_set_cert_cb(test_ctx.c_ctx, test_ech_cert_setup_cb, + &certSetupCalls); +#endif /* Generate ECH config with public_name matching the server cert SAN */ ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, publicName, @@ -19093,12 +19314,71 @@ static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) * With FAIL_IF_NO_PEER_CERT set, the server aborts with NO_PEER_CERT. */ ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), WC_NO_ERR_TRACE(NO_PEER_CERT)); +#ifdef WOLFSSL_CERT_SETUP_CB + ExpectIntEQ(certSetupCalls, 0); +#endif test_ssl_memio_cleanup(&test_ctx); return EXPECT_RESULT(); } +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) +static int echClientCertCbCalls = 0; + +/* Client cert callback: counts calls and supplies no certificate */ +static int test_ech_client_cert_cb(WOLFSSL* ssl, WOLFSSL_X509** x509, + WOLFSSL_EVP_PKEY** pkey) +{ + (void)ssl; + (void)x509; + (void)pkey; + echClientCertCbCalls++; + return 0; +} +#endif + +/* when ECH is rejected an empty cert should be sent, check that the cb does not + * load the client cert anyway */ +static int test_wolfSSL_Tls13_ECH_rejected_client_cert_cb(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) + test_ssl_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + echClientCertCbCalls = 0; + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.c_cb.certPemFile = ""; + test_ctx.c_cb.keyPemFile = ""; + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + wolfSSL_CTX_set_client_cert_cb(test_ctx.c_ctx, test_ech_client_cert_cb); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* request a client cert, reject ECH and match the server SNI to the + * public name */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_PEER, NULL); + wolfSSL_SetEchEnable(test_ctx.s_ssl, 0); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + echPublicName, (word16)XSTRLEN(echPublicName)), WOLFSSL_SUCCESS); + + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_GetEchStatus(test_ctx.c_ssl), + WOLFSSL_ECH_STATUS_REJECTED); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + ExpectIntEQ(echClientCertCbCalls, 0); + + test_ssl_memio_cleanup(&test_ctx); +#endif + return EXPECT_RESULT(); +} + /* Install ECH on the server and initialize the ECH AAD, then run TLSX_Parse. * ECH acceptance will be determined during TLSX_Parse */ static int ech_server_parse_outer_ch(WOLFSSL* ssl, byte* chRecord) @@ -45790,6 +46070,10 @@ TEST_CASE testCases[] = { TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_decrypt_error), TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_cert_valid), TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_empty_client_cert), + TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_client_cert_cb), + TEST_DECL(test_wolfSSL_Tls13_ECH_tls12_downgrade), + TEST_DECL(test_wolfSSL_Tls13_ECH_tls12_client), + TEST_DECL(test_wolfSSL_Tls13_ECH_tls12_resume), TEST_DECL(test_wolfSSL_Tls13_ECH_sni_parse), TEST_DECL(test_wolfSSL_Tls13_ECH_read_ahead), #endif From d212600ac288d9b0a0f63e15dcffdc5ce70fd2ad Mon Sep 17 00:00:00 2001 From: sebastian-carpenter Date: Tue, 27 Oct 2026 16:24:50 -0600 Subject: [PATCH 2/2] address feedback for downgrade --- src/internal.c | 45 +++++++++++++++---------------------------- src/ssl_api_hs.c | 3 +-- src/ssl_ech.c | 7 +++++++ src/tls13.c | 48 ++++++++++++++++++---------------------------- tests/api.c | 35 +++++++++++++++++++++++---------- wolfssl/internal.h | 6 ++++++ 6 files changed, 73 insertions(+), 71 deletions(-) diff --git a/src/internal.c b/src/internal.c index 3877fc1f022..bf98d26719e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -20119,8 +20119,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* RFC 9849 s6.1.7: ECH offered but rejected by the server... * verify cert is valid for ECHConfig.public_name */ if (ssl->options.side == WOLFSSL_CLIENT_END && - ssl->echConfigs != NULL && - !ssl->options.echAccepted) { + EchRejected(ssl)) { TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); if (echX != NULL && echX->data != NULL) { WOLFSSL_ECH* ech = (WOLFSSL_ECH*)echX->data; @@ -20681,14 +20680,9 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, /* Drop the unverified ticket (SetTicket() made the session * unique, so this cannot clear one shared with the app). */ if (ssl->options.side == WOLFSSL_CLIENT_END && - ssl->msgsReceived.got_session_ticket - #ifdef HAVE_ECH + ssl->msgsReceived.got_session_ticket && /* no ticket was stored when ECH is rejected */ - && !(ssl->echConfigs != NULL && - !ssl->options.disableECH && - !ssl->options.echAccepted) - #endif - ) { + !EchRejected(ssl)) { ForceZero(ssl->session->ticket, ssl->session->ticketLen); if (ssl->session->ticketLenAlloc > 0) { XFREE(ssl->session->ticket, ssl->heap, @@ -20747,12 +20741,8 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, ssl->session->ticketLen > 0) #endif ) -#ifdef HAVE_ECH /* ignore the session when ECH is rejected */ - && !(ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) -#endif - ) { + && !EchRejected(ssl)) { SetupSession(ssl); #ifndef NO_SESSION_CACHE AddSession(ssl); @@ -36822,12 +36812,9 @@ static int IsTls13CipherSuite(byte first, byte second) } if ( - #ifdef HAVE_ECH /* no client cert is sent when ECH is rejected, so an error with * picking the alg would reveal information about the cert */ - !(ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) && - #endif + !EchRejected(ssl) && PickHashSigAlgo(ssl, input + *inOutIdx, len, 0) != 0 && ssl->buffers.certificate && ssl->buffers.certificate->buffer) { @@ -36935,8 +36922,7 @@ static int IsTls13CipherSuite(byte first, byte second) #if defined(HAVE_ECH) /* RFC 9849 s6.1.7: ECH was offered but rejected by the server... * the client MUST respond with an empty Certificate message. */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { ssl->options.sendVerify = SEND_BLANK_CERT; WOLFSSL_LEAVE("DoCertificateRequest", 0); WOLFSSL_END(WC_FUNC_CERTIFICATE_REQUEST_DO); @@ -40040,16 +40026,6 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, return SESSION_TICKET_EXPECT_E; } -#ifdef HAVE_ECH - /* ignore session ticket when ECH is rejected */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { - ssl->expect_session_ticket = 0; - *inOutIdx += size; - return 0; - } -#endif - /* A renewed ticket while resuming confirms resumption; check before the * SetupSession() in DoFinished refreshes the cached suite/EMS and masks a * downgrade. @@ -40075,6 +40051,15 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, if ((*inOutIdx - begin) + length > size) return BUFFER_ERROR; +#ifdef HAVE_ECH + /* ignore session ticket when ECH is rejected */ + if (EchRejected(ssl)) { + *inOutIdx += length; + ssl->expect_session_ticket = 0; + return 0; + } +#endif + if ((ret = SetTicket(ssl, input + *inOutIdx, length)) != 0) return ret; *inOutIdx += length; diff --git a/src/ssl_api_hs.c b/src/ssl_api_hs.c index ec8e4c8ba9a..5feaff1d091 100644 --- a/src/ssl_api_hs.c +++ b/src/ssl_api_hs.c @@ -692,8 +692,7 @@ int wolfSSL_connect(WOLFSSL* ssl) #if defined(HAVE_ECH) /* RFC 9849 s6.1.4: negotiating below TLS 1.3 means the server rejected * ECH. Abort with ech_required instead of reporting success. */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { SendAlert(ssl, alert_fatal, ech_required); ssl->error = ECH_REQUIRED_E; WOLFSSL_ERROR_VERBOSE(ECH_REQUIRED_E); diff --git a/src/ssl_ech.c b/src/ssl_ech.c index cd48588802c..5149f51ebd1 100644 --- a/src/ssl_ech.c +++ b/src/ssl_ech.c @@ -915,6 +915,13 @@ int GetEchConfigsEx(WOLFSSL_EchConfig* configs, byte* output, word32* outputLen) return WOLFSSL_SUCCESS; } +/* Returns 1 if ECH was offered and not accepted by the server */ +int EchRejected(const WOLFSSL* ssl) +{ + return !ssl->options.disableECH && ssl->echConfigs != NULL && + !ssl->options.echAccepted; +} + #endif /* WOLFSSL_TLS13 && HAVE_ECH */ #endif /* !WOLFSSL_SSL_ECH_INCLUDED */ diff --git a/src/tls13.c b/src/tls13.c index 50ef0527136..b52e374210d 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -5199,7 +5199,7 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* if ECH was rejected by the HRR then the server MUST stop * decrypting ECH, so send a GREASE ECH for the follow-up CH */ - if (ssl->echConfigs != NULL && !ssl->options.echAccepted && + if (EchRejected(ssl) && ssl->options.serverState == SERVER_HELLO_RETRY_REQUEST_COMPLETE) { args->ech->state = ECH_WRITE_GREASE; @@ -6429,8 +6429,7 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, else { #if defined(HAVE_ECH) /* do not resume when outerHandshake will be negotiated */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { WOLFSSL_MSG("ECH rejected but server negotiated PSK"); return INVALID_PARAMETER; } @@ -6733,11 +6732,8 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, #ifdef WOLFSSL_CERT_SETUP_CB if ( - #ifdef HAVE_ECH /* no client cert is sent when ECH is rejected */ - !(ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) && - #endif + !EchRejected(ssl) && (ret = CertSetupCbWrapper(ssl)) != 0) return ret; #endif @@ -6745,8 +6741,7 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, #if defined(HAVE_ECH) /* RFC 9849 s6.1.7: ECH was offered but rejected by the server... * the client MUST respond with an empty Certificate message. */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { ssl->options.sendVerify = SEND_BLANK_CERT; } else @@ -10807,11 +10802,8 @@ static int SendTls13Certificate(WOLFSSL* ssl) #if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) /* call client cert callback if no cert has been loaded */ if ((ssl->ctx->CBClientCert != NULL) && - #ifdef HAVE_ECH /* no client cert is sent when ECH is rejected */ - !(ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) && - #endif + !EchRejected(ssl) && (!ssl->buffers.certificate || !ssl->buffers.certificate->buffer)) { ret = ssl->ctx->CBClientCert(ssl, &x509, &pkey); if (ret == 1) { @@ -14684,8 +14676,7 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, #ifdef HAVE_ECH /* ignore session ticket when ECH is rejected */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { *inOutIdx += size; return 0; } @@ -17055,23 +17046,10 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) FALL_THROUGH; case FINISHED_DONE: - #ifndef NO_HANDSHAKE_DONE_CB - if (ssl->hsDoneCb != NULL) { - int cbret = ssl->hsDoneCb(ssl, ssl->hsDoneCtx); - if (cbret < 0) { - ssl->error = cbret; - WOLFSSL_ERROR_VERBOSE(ssl->error); - WOLFSSL_MSG("HandShake Done Cb don't continue error"); - return WOLFSSL_FATAL_ERROR; - } - } - #endif /* NO_HANDSHAKE_DONE_CB */ - #if defined(HAVE_ECH) /* RFC 9849 s6.1.6: if we offered ECH but the server rejected it, * send ech_required alert and abort before returning to the app */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH && - !ssl->options.echAccepted) { + if (EchRejected(ssl)) { if (ssl->echRetryConfigs != NULL) { ssl->options.echRetryConfigsAccepted = 1; } @@ -17082,6 +17060,18 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) } #endif /* HAVE_ECH */ + #ifndef NO_HANDSHAKE_DONE_CB + if (ssl->hsDoneCb != NULL) { + int cbret = ssl->hsDoneCb(ssl, ssl->hsDoneCtx); + if (cbret < 0) { + ssl->error = cbret; + WOLFSSL_ERROR_VERBOSE(ssl->error); + WOLFSSL_MSG("HandShake Done Cb don't continue error"); + return WOLFSSL_FATAL_ERROR; + } + } + #endif /* NO_HANDSHAKE_DONE_CB */ + if (!ssl->options.keepResources) { FreeHandshakeResources(ssl); } diff --git a/tests/api.c b/tests/api.c index 8f54895d4de..7f499b5fb22 100644 --- a/tests/api.c +++ b/tests/api.c @@ -18966,7 +18966,7 @@ static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void) * ech config * the cert check should pass and the client aborts with ech_required */ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, - int validName, method_provider serverMethod) + int validName, method_provider clientMethod, method_provider serverMethod) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; @@ -18976,7 +18976,7 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, XMEMSET(&test_ctx, 0, sizeof(test_ctx)); test_ctx.s_cb.method = serverMethod; - test_ctx.c_cb.method = wolfSSLv23_client_method; + test_ctx.c_cb.method = clientMethod; ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); @@ -19040,14 +19040,14 @@ static int test_wolfSSL_Tls13_ECH_rejected_cert_valid(void) /* "example.com" appears in the SAN of certs/server-cert.pem */ ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("example.com", 1, - wolfTLSv1_3_server_method), TEST_SUCCESS); + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), TEST_SUCCESS); ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0, - wolfTLSv1_3_server_method), TEST_SUCCESS); + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), TEST_SUCCESS); #ifndef WOLFSSL_NO_TLS12 /* a downgrade to TLS 1.2 also rejects ECH, the good name is covered by * test_wolfSSL_Tls13_ECH_tls12_downgrade */ ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0, - wolfTLSv1_2_server_method), TEST_SUCCESS); + wolfSSLv23_client_method, wolfTLSv1_2_server_method), TEST_SUCCESS); #endif return EXPECT_RESULT(); @@ -19336,21 +19336,20 @@ static int test_ech_client_cert_cb(WOLFSSL* ssl, WOLFSSL_X509** x509, echClientCertCbCalls++; return 0; } -#endif /* when ECH is rejected an empty cert should be sent, check that the cb does not * load the client cert anyway */ -static int test_wolfSSL_Tls13_ECH_rejected_client_cert_cb(void) +static int test_wolfSSL_Tls13_ECH_rejected_client_cert_cb_ex( + method_provider clientMethod, method_provider serverMethod) { EXPECT_DECLS; -#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) test_ssl_memio_ctx test_ctx; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); echClientCertCbCalls = 0; - test_ctx.s_cb.method = wolfTLSv1_3_server_method; - test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.method = serverMethod; + test_ctx.c_cb.method = clientMethod; test_ctx.c_cb.certPemFile = ""; test_ctx.c_cb.keyPemFile = ""; test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; @@ -19375,6 +19374,22 @@ static int test_wolfSSL_Tls13_ECH_rejected_client_cert_cb(void) ExpectIntEQ(echClientCertCbCalls, 0); test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} +#endif + +static int test_wolfSSL_Tls13_ECH_rejected_client_cert_cb(void) +{ + EXPECT_DECLS; +#if defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_SETUP_CB) + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_client_cert_cb_ex( + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), TEST_SUCCESS); +#ifndef WOLFSSL_NO_TLS12 + /* a downgrade to TLS 1.2 also rejects ECH */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_client_cert_cb_ex( + wolfSSLv23_client_method, wolfTLSv1_2_server_method), TEST_SUCCESS); +#endif #endif return EXPECT_RESULT(); } diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 6f9c8cddb6e..229697c83ec 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3580,6 +3580,8 @@ WOLFSSL_LOCAL void FreeEchConfigs(WOLFSSL_EchConfig* configs, void* heap); WOLFSSL_LOCAL int SetRetryConfigs(WOLFSSL* ssl, const byte* echConfigs, word32 echConfigsLen); + +WOLFSSL_LOCAL int EchRejected(const WOLFSSL* ssl); #endif struct TLSX { @@ -3675,6 +3677,10 @@ WOLFSSL_LOCAL int TLSX_Append(TLSX** list, TLSX_Type type, #endif #endif /* HAVE_TLS_EXTENSIONS */ +#if !defined(WOLFSSL_TLS13) || !defined(HAVE_ECH) + #define EchRejected(ssl) 0 +#endif + /** Server Name Indication - RFC 6066 (session 3) */ #ifdef HAVE_SNI