From b174b8b326622574dd49fce71589f414f6af937f Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Thu, 7 May 2026 23:07:54 +0200 Subject: [PATCH] ECH: cleanups - passing an unknown string to CURLOPT_ECH now returns error To properly allow applications to spot if they pass in a typo or something to libcurl. - CURLECH_DISABLE is now a plain zero internally, not a dedicated bit which simplifies checks for when ECH is enabled - Dropped the CURLECH_CLA_CFG bit, and just check STRING_ECH_CONFIG - Turn grease/enable/hard into three different numerical values, no bitmask needed - Convert the struct field 'tls_ech' from an int to a byte. Closes #21532 --- lib/setopt.c | 73 +++++++++++++++++++++---------------- lib/urldata.h | 6 +-- lib/vtls/openssl.c | 22 +++++------ lib/vtls/rustls.c | 12 +++--- lib/vtls/vtls.h | 14 +++---- lib/vtls/wolfssl.c | 15 ++++---- tests/libtest/mk-lib1521.pl | 1 + 7 files changed, 75 insertions(+), 68 deletions(-) diff --git a/lib/setopt.c b/lib/setopt.c index 59d3c3f616..f481614dc1 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -1858,6 +1858,45 @@ static CURLcode setopt_copypostfields(const char *ptr, struct UserDefined *s) } #endif +#ifdef USE_ECH +static CURLcode setopt_ech(struct Curl_easy *data, const char *ptr) +{ + struct UserDefined *s = &data->set; + CURLcode result = CURLE_OK; + + if(!ptr || !strcmp(ptr, "false")) + s->tls_ech = CURLECH_DISABLE; + else { + size_t plen = strlen(ptr); + if(plen > CURL_MAX_INPUT_LENGTH) + result = CURLE_BAD_FUNCTION_ARGUMENT; + else { + if(!strcmp(ptr, "grease")) + s->tls_ech = CURLECH_GREASE; + else if(!strcmp(ptr, "true")) + s->tls_ech = CURLECH_ENABLE; + else if(!strcmp(ptr, "hard")) + s->tls_ech = CURLECH_HARD; + else if(plen > 4 && !strncmp(ptr, "ecl:", 4)) { + if(!s->tls_ech) + s->tls_ech = CURLECH_HARD; + result = Curl_setstropt(&s->str[STRING_ECH_CONFIG], ptr + 4); + } + else if(plen > 3 && !strncmp(ptr, "pn:", 3)) { + if(!s->tls_ech) + s->tls_ech = CURLECH_HARD; + result = Curl_setstropt(&s->str[STRING_ECH_PUBLIC], ptr + 3); + } + else + result = CURLE_BAD_FUNCTION_ARGUMENT; + } + } + return result; +} +#else +#define setopt_ech(x,y) CURLE_NOT_BUILT_IN +#endif + static CURLcode setopt_cptr(struct Curl_easy *data, CURLoption option, char *ptr) { @@ -2495,38 +2534,8 @@ static CURLcode setopt_cptr(struct Curl_easy *data, CURLoption option, return Curl_altsvc_load(data->asi, ptr); break; #endif /* !CURL_DISABLE_ALTSVC */ -#ifdef USE_ECH - case CURLOPT_ECH: { - size_t plen = 0; - - if(!ptr) { - s->tls_ech = CURLECH_DISABLE; - break; - } - plen = strlen(ptr); - if(plen > CURL_MAX_INPUT_LENGTH) { - s->tls_ech = CURLECH_DISABLE; - return CURLE_BAD_FUNCTION_ARGUMENT; - } - /* set tls_ech flag value, preserving CLA_CFG bit */ - if(!strcmp(ptr, "false")) - s->tls_ech = (s->tls_ech & CURLECH_CLA_CFG) | CURLECH_DISABLE; - else if(!strcmp(ptr, "grease")) - s->tls_ech = (s->tls_ech & CURLECH_CLA_CFG) | CURLECH_GREASE; - else if(!strcmp(ptr, "true")) - s->tls_ech = (s->tls_ech & CURLECH_CLA_CFG) | CURLECH_ENABLE; - else if(!strcmp(ptr, "hard")) - s->tls_ech = (s->tls_ech & CURLECH_CLA_CFG) | CURLECH_HARD; - else if(plen > 5 && !strncmp(ptr, "ecl:", 4)) { - result = Curl_setstropt(&s->str[STRING_ECH_CONFIG], ptr + 4); - if(!result) - s->tls_ech |= CURLECH_CLA_CFG; - } - else if(plen > 4 && !strncmp(ptr, "pn:", 3)) - result = Curl_setstropt(&s->str[STRING_ECH_PUBLIC], ptr + 3); - break; - } -#endif + case CURLOPT_ECH: + return setopt_ech(data, ptr); default: return CURLE_UNKNOWN_OPTION; } diff --git a/lib/urldata.h b/lib/urldata.h index c06865feee..7fff77c2b3 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -1157,9 +1157,6 @@ struct UserDefined { struct curl_slist *mail_rcpt; /* linked list of mail recipients */ #endif uint32_t maxconnects; /* Max idle connections in the connection cache */ -#ifdef USE_ECH - int tls_ech; /* TLS ECH configuration */ -#endif short maxredirs; /* maximum no. of http(s) redirects to follow, set to -1 for infinity */ uint16_t expect_100_timeout; /* in milliseconds */ @@ -1174,6 +1171,9 @@ struct UserDefined { #ifndef CURL_DISABLE_TFTP uint16_t tftp_blksize; /* in bytes, 0 means use default */ #endif +#ifdef USE_ECH + uint8_t tls_ech; /* TLS ECH configuration */ +#endif #ifndef CURL_DISABLE_NETRC uint8_t use_netrc; /* enum CURL_NETRC_OPTION values */ #endif diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 30b5c1e2e9..0e9796f009 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -3430,9 +3430,9 @@ bool Curl_ossl_need_httpsrr(struct Curl_easy *data) { if(!CURLECH_ENABLED(data)) return FALSE; - if((data->set.tls_ech & CURLECH_GREASE) || - (data->set.tls_ech & CURLECH_CLA_CFG)) - return FALSE; + if((data->set.tls_ech == CURLECH_GREASE) || + data->set.str[STRING_ECH_CONFIG]) + return FALSE; return TRUE; } @@ -3450,7 +3450,7 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, if(!CURLECH_ENABLED(data)) return CURLE_OK; - if(data->set.tls_ech & CURLECH_GREASE) { + if(data->set.tls_ech == CURLECH_GREASE) { infof(data, "ECH: will GREASE ClientHello"); #ifdef HAVE_BORINGSSL_LIKE SSL_set_enable_ech_grease(octx->ssl, 1); @@ -3458,7 +3458,7 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, SSL_set_options(octx->ssl, SSL_OP_ECH_GREASE); #endif } - else if(data->set.tls_ech & CURLECH_CLA_CFG) { + else if(data->set.tls_ech && data->set.str[STRING_ECH_CONFIG]) { #ifdef HAVE_BORINGSSL_LIKE /* have to do base64 decode here for BoringSSL */ const char *b64 = data->set.str[STRING_ECH_CONFIG]; @@ -3471,12 +3471,12 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, result = curlx_base64_decode(b64, &ech_config, &ech_config_len); if(result || !ech_config) { infof(data, "ECH: cannot base64 decode ECHConfig from command line"); - if(data->set.tls_ech & CURLECH_HARD) + if(data->set.tls_ech == CURLECH_HARD) return result; } if(SSL_set1_ech_config_list(octx->ssl, ech_config, ech_config_len) != 1) { infof(data, "ECH: SSL_ECH_set1_ech_config_list failed"); - if(data->set.tls_ech & CURLECH_HARD) { + if(data->set.tls_ech == CURLECH_HARD) { curlx_free(ech_config); return CURLE_SSL_CONNECT_ERROR; } @@ -3492,7 +3492,7 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, ech_config_len = strlen(data->set.str[STRING_ECH_CONFIG]); if(SSL_set1_ech_config_list(octx->ssl, ech_config, ech_config_len) != 1) { infof(data, "ECH: SSL_ECH_set1_ech_config_list failed"); - if(data->set.tls_ech & CURLECH_HARD) + if(data->set.tls_ech == CURLECH_HARD) return CURLE_SSL_CONNECT_ERROR; } else @@ -3511,7 +3511,7 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, infof(data, "ECH: ECHConfig from HTTPS RR"); if(SSL_set1_ech_config_list(octx->ssl, ecl, elen) != 1) { infof(data, "ECH: SSL_set1_ech_config_list failed"); - if(data->set.tls_ech & CURLECH_HARD) + if(data->set.tls_ech == CURLECH_HARD) return CURLE_SSL_CONNECT_ERROR; } else { @@ -3521,7 +3521,7 @@ static CURLcode ossl_init_ech(struct ossl_ctx *octx, } else { infof(data, "ECH: requested but no ECHConfig available"); - if(data->set.tls_ech & CURLECH_HARD) + if(data->set.tls_ech == CURLECH_HARD) return CURLE_SSL_CONNECT_ERROR; } } @@ -4335,7 +4335,7 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf, /* trace retry_configs if we got some */ ossl_trace_ech_retry_configs(data, octx->ssl, 0); } - if(rv != SSL_ECH_STATUS_SUCCESS && (data->set.tls_ech & CURLECH_HARD)) { + if(rv != SSL_ECH_STATUS_SUCCESS && (data->set.tls_ech == CURLECH_HARD)) { infof(data, "ECH: ech-hard failed"); return CURLE_SSL_CONNECT_ERROR; } diff --git a/lib/vtls/rustls.c b/lib/vtls/rustls.c index 8c56ede7fc..24b8597045 100644 --- a/lib/vtls/rustls.c +++ b/lib/vtls/rustls.c @@ -915,9 +915,9 @@ static bool cr_ech_need_httpsrr(struct Curl_easy *data) { if(!CURLECH_ENABLED(data)) return FALSE; - if((data->set.tls_ech & CURLECH_GREASE) || - (data->set.tls_ech & CURLECH_CLA_CFG)) - return FALSE; + if((data->set.tls_ech == CURLECH_GREASE) || + data->set.str[STRING_ECH_CONFIG]) + return FALSE; return TRUE; } @@ -957,7 +957,7 @@ init_config_builder_ech(struct Curl_easy *data, return CURLE_OK; } - if(data->set.tls_ech & CURLECH_CLA_CFG && data->set.str[STRING_ECH_CONFIG]) { + if(data->set.tls_ech && data->set.str[STRING_ECH_CONFIG]) { const char *b64 = data->set.str[STRING_ECH_CONFIG]; size_t decode_result; if(!b64) { @@ -997,7 +997,7 @@ init_config_builder_ech(struct Curl_easy *data, } cleanup: /* if we base64 decoded, we can free now */ - if(data->set.tls_ech & CURLECH_CLA_CFG && data->set.str[STRING_ECH_CONFIG]) { + if(data->set.tls_ech && data->set.str[STRING_ECH_CONFIG]) { curlx_free(ech_config); } if(dns) { @@ -1074,7 +1074,7 @@ static CURLcode cr_init_backend(struct Curl_cfilter *cf, #ifdef USE_ECH if(CURLECH_ENABLED(data)) { result = init_config_builder_ech(data, cf, config_builder); - if(result != CURLE_OK && data->set.tls_ech & CURLECH_HARD) { + if((result != CURLE_OK) && (data->set.tls_ech == CURLECH_HARD)) { rustls_client_config_builder_free(config_builder); return result; } diff --git a/lib/vtls/vtls.h b/lib/vtls/vtls.h index 5493316954..484696dffe 100644 --- a/lib/vtls/vtls.h +++ b/lib/vtls/vtls.h @@ -49,15 +49,13 @@ struct dynbuf; #define SSLSUPP_ISSUERCERT_BLOB (1 << 14) /* CURLOPT_ISSUERCERT_BLOB */ #ifdef USE_ECH -/* CURLECH_ bits for the tls_ech option */ -#define CURLECH_DISABLE (1 << 0) -#define CURLECH_GREASE (1 << 1) -#define CURLECH_ENABLE (1 << 2) -#define CURLECH_HARD (1 << 3) -#define CURLECH_CLA_CFG (1 << 4) +/* CURLECH_ values for the tls_ech option */ +#define CURLECH_DISABLE 0 +#define CURLECH_GREASE 1 +#define CURLECH_ENABLE 2 +#define CURLECH_HARD 3 -#define CURLECH_ENABLED(data) \ - ((data)->set.tls_ech && !((data)->set.tls_ech & CURLECH_DISABLE)) +#define CURLECH_ENABLED(data) ((data)->set.tls_ech) #endif /* USE_ECH */ #define ALPN_ACCEPTED "ALPN: server accepted " diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c index 7de03b36d5..59574c9b6a 100644 --- a/lib/vtls/wolfssl.c +++ b/lib/vtls/wolfssl.c @@ -1273,15 +1273,14 @@ static CURLcode wssl_init_ech(struct wssl_ctx *wctx, infof(data, "ECH: GREASE is done by default by" " wolfSSL: no need to ask"); } - if(data->set.tls_ech & CURLECH_CLA_CFG && - data->set.str[STRING_ECH_CONFIG]) { + if(data->set.tls_ech && data->set.str[STRING_ECH_CONFIG]) { char *b64val = data->set.str[STRING_ECH_CONFIG]; word32 b64len = 0; b64len = (word32)strlen(b64val); if(b64len && wolfSSL_SetEchConfigsBase64(wctx->ssl, b64val, b64len) != WOLFSSL_SUCCESS) { - if(data->set.tls_ech & CURLECH_HARD) + if(data->set.tls_ech == CURLECH_HARD) return CURLE_SSL_CONNECT_ERROR; } else { @@ -1301,7 +1300,7 @@ static CURLcode wssl_init_ech(struct wssl_ctx *wctx, if(wolfSSL_SetEchConfigs(wctx->ssl, ecl, (word32)elen) != WOLFSSL_SUCCESS) { infof(data, "ECH: wolfSSL_SetEchConfigs failed"); - if(data->set.tls_ech & CURLECH_HARD) { + if(data->set.tls_ech == CURLECH_HARD) { return CURLE_SSL_CONNECT_ERROR; } } @@ -1312,7 +1311,7 @@ static CURLcode wssl_init_ech(struct wssl_ctx *wctx, } else { infof(data, "ECH: requested but no ECHConfig available"); - if(data->set.tls_ech & CURLECH_HARD) { + if(data->set.tls_ech == CURLECH_HARD) { return CURLE_SSL_CONNECT_ERROR; } } @@ -1492,9 +1491,9 @@ bool Curl_wssl_need_httpsrr(struct Curl_easy *data) #ifdef HAVE_WOLFSSL_CTX_GENERATEECHCONFIG if(!CURLECH_ENABLED(data)) return FALSE; - if((data->set.tls_ech & CURLECH_GREASE) || - (data->set.tls_ech & CURLECH_CLA_CFG)) - return FALSE; + if((data->set.tls_ech == CURLECH_GREASE) || + data->set.str[STRING_ECH_CONFIG]) + return FALSE; return TRUE; #else (void)data; diff --git a/tests/libtest/mk-lib1521.pl b/tests/libtest/mk-lib1521.pl index f3d0c088e8..2e7a01835a 100755 --- a/tests/libtest/mk-lib1521.pl +++ b/tests/libtest/mk-lib1521.pl @@ -42,6 +42,7 @@ my @bad_function_argument = ( 'CURLOPT_DNS_LOCAL_IP4', 'CURLOPT_DNS_LOCAL_IP6', 'CURLOPT_DNS_SERVERS', + 'CURLOPT_ECH', 'CURLOPT_PROXY_TLSAUTH_TYPE', 'CURLOPT_SSLENGINE', 'CURLOPT_TLSAUTH_TYPE',