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.
This commit is contained in:
sftcd 2025-09-26 22:10:36 +01:00 committed by William Lallemand
parent 83e3cbc262
commit dba4fd248a
5 changed files with 139 additions and 2 deletions

View File

@ -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

11
include/haproxy/ech.h Normal file
View File

@ -0,0 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#ifndef _HAPROXY_ECH_H
# define _HAPROXY_ECH_H
#ifdef USE_ECH
#include <openssl/ech.h>
int load_echkeys(SSL_CTX *ctx, char *dirname, int *loaded);
# endif /* USE_ECH */
#endif /* _HAPROXY_ECH_H */

View File

@ -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
};

100
src/ech.c Normal file
View File

@ -0,0 +1,100 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#ifdef USE_ECH
#include <dirent.h>
#include <sys/stat.h>
#include <haproxy/applet.h>
#include <haproxy/cli.h>
#include <haproxy/ech.h>
#include <haproxy/fd.h>
#include <haproxy/global.h>
#include <haproxy/listener.h>
#include <haproxy/log.h>
#include <haproxy/obj_type.h>
#include <haproxy/openssl-compat.h>
#include <haproxy/proxy.h>
#include <haproxy/ssl_sock-t.h>
/*
* load any key files called <name>.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

View File

@ -90,7 +90,9 @@
#include <haproxy/ssl_ocsp.h>
#include <haproxy/trace.h>
#include <haproxy/ssl_trace.h>
#ifdef USE_ECH
#include <haproxy/ech.h>
#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
}
}