From dba4fd248a13fb0f3135619b14e3cf20b6674d10 Mon Sep 17 00:00:00 2001 From: sftcd Date: Fri, 26 Sep 2025 22:10:36 +0100 Subject: [PATCH] MEDIUM: ssl/ech: config and load keys This patch introduces the USE_ECH option in the Makefile to enable support for Encrypted Client Hello (ECH) with OpenSSL. A new function, load_echkeys, is added to load ECH keys from a specified directory. The SSL context initialization process in ssl_sock.c is updated to load these keys if configured. A new configuration directive, `ech`, is introduced to allow users to specify the ECH key directory in the listener configuration. --- Makefile | 4 +- include/haproxy/ech.h | 11 ++++ include/haproxy/listener-t.h | 3 ++ src/ech.c | 100 +++++++++++++++++++++++++++++++++++ src/ssl_sock.c | 23 +++++++- 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 include/haproxy/ech.h create mode 100644 src/ech.c diff --git a/Makefile b/Makefile index d26e57948..fffc4f4f3 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ # USE_OPENSSL : enable use of OpenSSL. Recommended, but see below. # USE_OPENSSL_AWSLC : enable use of AWS-LC # USE_OPENSSL_WOLFSSL : enable use of wolfSSL with the OpenSSL API +# USE_ECH : enable use of ECH with the OpenSSL API # USE_QUIC : enable use of QUIC with the quictls API (quictls, libressl, boringssl) # USE_QUIC_OPENSSL_COMPAT : enable use of QUIC with the standard openssl API (limited features) # USE_ENGINE : enable use of OpenSSL Engine. @@ -341,6 +342,7 @@ use_opts = USE_EPOLL USE_KQUEUE USE_NETFILTER USE_POLL \ USE_TPROXY USE_LINUX_TPROXY USE_LINUX_CAP \ USE_LINUX_SPLICE USE_LIBCRYPT USE_CRYPT_H USE_ENGINE \ USE_GETADDRINFO USE_OPENSSL USE_OPENSSL_WOLFSSL USE_OPENSSL_AWSLC \ + USE_ECH \ USE_SSL USE_LUA USE_ACCEPT4 USE_CLOSEFROM USE_ZLIB USE_SLZ \ USE_CPU_AFFINITY USE_TFO USE_NS USE_DL USE_RT USE_LIBATOMIC \ USE_MATH USE_DEVICEATLAS USE_51DEGREES \ @@ -1000,7 +1002,7 @@ OBJS += src/mux_h2.o src/mux_h1.o src/mux_fcgi.o src/log.o \ src/ebsttree.o src/freq_ctr.o src/systemd.o src/init.o \ src/http_acl.o src/dict.o src/dgram.o src/pipe.o \ src/hpack-huff.o src/hpack-enc.o src/ebtree.o src/hash.o \ - src/httpclient_cli.o src/version.o src/ncbmbuf.o + src/httpclient_cli.o src/version.o src/ncbmbuf.o src/ech.o ifneq ($(TRACE),) OBJS += src/calltrace.o diff --git a/include/haproxy/ech.h b/include/haproxy/ech.h new file mode 100644 index 000000000..dac74cfba --- /dev/null +++ b/include/haproxy/ech.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#ifndef _HAPROXY_ECH_H +# define _HAPROXY_ECH_H +#ifdef USE_ECH + +#include + +int load_echkeys(SSL_CTX *ctx, char *dirname, int *loaded); + +# endif /* USE_ECH */ +#endif /* _HAPROXY_ECH_H */ diff --git a/include/haproxy/listener-t.h b/include/haproxy/listener-t.h index 0fbed6c7f..ce189736d 100644 --- a/include/haproxy/listener-t.h +++ b/include/haproxy/listener-t.h @@ -152,6 +152,9 @@ struct ssl_bind_conf { char *client_sigalgs; /* Client Signature algorithms */ struct tls_version_filter ssl_methods_cfg; /* original ssl methods found in configuration */ struct tls_version_filter ssl_methods; /* actual ssl methods used at runtime */ +#ifdef USE_ECH + char *ech_filedir; /* ECH config, file/directory name */ +#endif #endif }; diff --git a/src/ech.c b/src/ech.c new file mode 100644 index 000000000..69b522c3a --- /dev/null +++ b/src/ech.c @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#ifdef USE_ECH + + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/* + * load any key files called .ech we find in the named + * directory + */ +int load_echkeys(SSL_CTX *ctx, char *dirname, int *loaded) +{ + struct dirent **de_list = NULL; + struct stat thestat; + int rv = 0, i, nrv, somekeyworked = 0; + char *den = NULL, *last4 = NULL, privname[PATH_MAX]; + size_t elen = 0, nlen = 0; + OSSL_ECHSTORE * const es = OSSL_ECHSTORE_new(NULL, NULL); + + if (es == NULL) + goto end; + nrv = scandir(dirname, &de_list, 0, alphasort); + if (nrv < 0) + goto end; + for (i = 0; i != nrv; i++) { + struct dirent *de = de_list[i]; + + den = de->d_name; + nlen = strlen(den); + if (nlen > 4) { + last4 = den + nlen - 4; + if (strncmp(last4, ".ech", 4)) + goto ignore_entry; + if ((elen + 1 + nlen + 1) >= PATH_MAX) + goto ignore_entry; + snprintf(privname, PATH_MAX,"%s/%s", dirname, den); + if (stat(privname, &thestat) == 0) { + BIO *in = BIO_new_file(privname, "r"); + const int is_retry_config = OSSL_ECH_FOR_RETRY; + + if (in != NULL && 1 == OSSL_ECHSTORE_read_pem(es, in, is_retry_config)) + somekeyworked = 1; + BIO_free_all(in); + } + } +ignore_entry: + free(de); + } + + if (somekeyworked == 0) + goto end; + if (OSSL_ECHSTORE_num_keys(es, loaded) != 1) + goto end; + if (1 != SSL_CTX_set1_echstore(ctx, es)) + goto end; + rv = 1; +end: + free(de_list); + OSSL_ECHSTORE_free(es); + return rv; +} + + +static int bind_parse_ech(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) +{ + if (!experimental_directives_allowed) { + memprintf(err, "'%s' directive is experimental, must be allowed via a global 'expose-experimental-directives'", + args[0]); + return -1; + } + mark_tainted(TAINTED_CONFIG_EXP_KW_DECLARED); + + free(conf->ssl_conf.ech_filedir); + conf->ssl_conf.ech_filedir = strdup(args[cur_arg+1]); + return 0; +} + + +static struct bind_kw_list bind_kws = { "SSL", { }, { + { "ech", bind_parse_ech, 1 }, /* set ECH PEM file */ + { 0, NULL, 0 }, +}}; + + +INITCALL1(STG_REGISTER, bind_register_keywords, &bind_kws); +#endif diff --git a/src/ssl_sock.c b/src/ssl_sock.c index 14b817980..0cdd6c77e 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -90,7 +90,9 @@ #include #include #include - +#ifdef USE_ECH +#include +#endif /* ***** READ THIS before adding code here! ***** * @@ -3954,6 +3956,19 @@ ssl_sock_initial_ctx(struct bind_conf *bind_conf) if (global_ssl.security_level > -1) SSL_CTX_set_security_level(ctx, global_ssl.security_level); +#ifdef USE_ECH + if (bind_conf->ssl_conf.ech_filedir) { + int loaded = 0; + + if (load_echkeys(ctx, bind_conf->ssl_conf.ech_filedir, &loaded) != 1) { + cfgerr += 1; + ha_alert("Proxy '%s': failed to load ECH key s from %s for '%s' at [%s:%d].\n", + bind_conf->frontend->id, bind_conf->ssl_conf.ech_filedir, + bind_conf->arg, bind_conf->file, bind_conf->line); + } + } +#endif + if (conf_ssl_methods->flags && (conf_ssl_methods->min || conf_ssl_methods->max)) ha_warning("Proxy '%s': no-sslv3/no-tlsv1x are ignored for bind '%s' at [%s:%d]. " "Use only 'ssl-min-ver' and 'ssl-max-ver' to fix.\n", @@ -5320,9 +5335,15 @@ int ssl_sock_prepare_bind_conf(struct bind_conf *bind_conf) px->id, bind_conf->arg, bind_conf->file, bind_conf->line); } else { +#ifdef USE_ECH + if (!bind_conf->ssl_conf.ech_filedir) { +#endif ha_alert("Proxy '%s': no SSL certificate specified for bind '%s' at [%s:%d] (use 'crt').\n", px->id, bind_conf->arg, bind_conf->file, bind_conf->line); return -1; +#ifdef USE_ECH + } +#endif } }