From: William Lallemand Date: Fri, 31 Jul 2026 15:59:46 +0000 (+0000) Subject: MEDIUM: ech: implement a lighter ECH feature for AWS-LC X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=dcc738bdfb297afe7c81703ee416c54d10838544;p=thirdparty%2Fhaproxy.git MEDIUM: ech: implement a lighter ECH feature for AWS-LC This patch implements ECH with AWS-LC. AWS-LC supports a different ECH API than OpenSSL 4.0. AWS-LC does not implement an API to load a PEM ECH file, the ECHCONFIG section is parsed manually using PEM_read_bio() to feed the SSL_ECH_KEYS object. Runtime ECH store management ('show/add/set/del ssl ech') and ECH status/outer-SNI reporting are disabled under AWS-LC for now. Should fix issue #3333. --- diff --git a/doc/configuration.txt b/doc/configuration.txt index 71e78b221..111e8e52f 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -17470,7 +17470,11 @@ ech [ EXPERIMENTAL ] "expose-experimental-directives" option in the global section. It also necessitates an OpenSSL version that supports ECH ( https://github.com/openssl/openssl/tree/feature/ech), and HAProxy must be - compiled with USE_ECH=1. The ECH API of AWS-LC is not supported. + compiled with USE_ECH=1. + + AWS-LC is also supported, with a lighter implementation: runtime ECH store + management ("show/add/set/del ssl ech" on the CLI) and ECH status/outer-SNI + reporting are not available in that case. Example: $ openssl ech -public_name foobar.com -out /etc/haproxy/echkeydir/foobar.com.ech diff --git a/doc/management.txt b/doc/management.txt index 7077ad69b..ce07f2617 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -1890,7 +1890,8 @@ add ssl ech Necessitates an OpenSSL version that supports ECH, and HAProxy must be compiled with USE_ECH=1. This command is only supported on a CLI connection - running in experimental mode (see "experimental-mode on"). + running in experimental mode (see "experimental-mode on"). Not available + when HAProxy is built with AWS-LC. See also "show ssl ech" and "ech" in the Section 5.1 of the configuration manual. @@ -2233,7 +2234,8 @@ del ssl ech Necessitates an OpenSSL version that supports ECH, and HAProxy must be compiled with USE_ECH=1. This command is only supported on a CLI connection - running in experimental mode (see "experimental-mode on"). + running in experimental mode (see "experimental-mode on"). Not available + when HAProxy is built with AWS-LC. See also "show ssl ech", "add ssl ech" and "ech" in the Section 5.1 of the configuration manual. @@ -2856,7 +2858,8 @@ set ssl ech Necessitates an OpenSSL version that supports ECH, and HAProxy must be compiled with USE_ECH=1. This command is only supported on a CLI connection - running in experimental mode (see "experimental-mode on"). + running in experimental mode (see "experimental-mode on"). Not available + when HAProxy is built with AWS-LC. See also "show ssl ech", "add ssl ech" and "ech" in the Section 5.1 of the configuration manual. @@ -4027,7 +4030,8 @@ show ssl ech [] Necessitates an OpenSSL version that supports ECH, and HAProxy must be compiled with USE_ECH=1. This command is only supported on a CLI connection running in experimental - mode (see "experimental-mode on"). + mode (see "experimental-mode on"). Not available when HAProxy is built with + AWS-LC. See also "ech" in the Section 5.1 of the configuration manual. diff --git a/include/haproxy/ech.h b/include/haproxy/ech.h index 4defef2e7..ebad42b5d 100644 --- a/include/haproxy/ech.h +++ b/include/haproxy/ech.h @@ -3,7 +3,13 @@ # define _HAPROXY_ECH_H #ifdef USE_ECH -#include +#include + +# if defined(OPENSSL_IS_AWSLC) +# include +# else +# include +# endif int load_echkeys(SSL_CTX *ctx, char *dirname, int *loaded, char **err); int conn_get_ech_status(struct connection *conn, struct buffer *buf); diff --git a/src/ech.c b/src/ech.c index 439358b6d..047cdcd52 100644 --- a/src/ech.c +++ b/src/ech.c @@ -30,15 +30,33 @@ struct show_ech_ctx { } state; /* phase of the current dump */ }; +#if defined(OPENSSL_IS_AWSLC) +typedef SSL_ECH_KEYS ech_store; +#else typedef OSSL_ECHSTORE ech_store; +#endif /* load one ECH key file into . * Returns 1 on success, 0 on error, with a reason appended to *err. */ static int ech_store_load_file(ech_store *store, const char *filename, char **err) { - BIO *in; - int rv; +#if defined(OPENSSL_IS_AWSLC) + /* AWS-LC has no PEM-file ECH key loader of its own: parse the + * "PRIVATE KEY" PEM block holding the raw HPKE private key and the + * "ECHCONFIG" PEM block holding the raw ECHConfig ourselves, and add + * them to . + */ + BIO *in = NULL; + EVP_PKEY *pkey = NULL; + unsigned char *cfg_data = NULL; + size_t cfg_len = 0; + unsigned char priv_key[EVP_HPKE_MAX_PRIVATE_KEY_LENGTH]; + size_t priv_key_len = 0; + EVP_HPKE_KEY hpke_key; + int rv = 0; + + EVP_HPKE_KEY_zero(&hpke_key); in = BIO_new_file(filename, "r"); if (!in) { @@ -46,6 +64,95 @@ static int ech_store_load_file(ech_store *store, const char *filename, char **er * crypto library's error queue rather than leaving it in * errno; load_echkeys() drains that queue into *err. */ + memprintf(err, "%sunable to open ECH key file '%s'", + err && *err ? *err : "", filename); + goto end; + } + + /* read every PEM block in the file and keep the private key and the + * ECHConfigList wherever each happens to appear; the first match of + * each type wins. + */ + while (1) { + char *pem_name = NULL, *pem_header = NULL; + unsigned char *pem_data = NULL; + long pem_len = 0; + const unsigned char *p; + + if (!PEM_read_bio(in, &pem_name, &pem_header, &pem_data, &pem_len)) + break; + + if (!pkey && strcmp(pem_name, "PRIVATE KEY") == 0) { + p = pem_data; + pkey = d2i_AutoPrivateKey(NULL, &p, pem_len); + if (pkey && EVP_PKEY_id(pkey) == EVP_PKEY_X25519) { + priv_key_len = sizeof(priv_key); + if (!EVP_PKEY_get_raw_private_key(pkey, priv_key, &priv_key_len)) + priv_key_len = 0; + } + } else if (!cfg_data && strcmp(pem_name, "ECHCONFIG") == 0 && pem_len > 2) { + /* the "ECHCONFIG" PEM block holds a full ECHConfigList: a + * 2-byte length prefix followed by one ECHConfig. + * SSL_ECH_KEYS_add() wants the bare ECHConfig, so check + * that the prefix matches the block's length, then + * strip it off in place and take ownership of the buffer. + */ + size_t list_len = ((size_t)pem_data[0] << 8) | pem_data[1]; + + if (list_len == (size_t)pem_len - 2) { + memmove(pem_data, pem_data + 2, list_len); + cfg_data = pem_data; + cfg_len = list_len; + pem_data = NULL; + } + } + + OPENSSL_free(pem_name); + OPENSSL_free(pem_header); + OPENSSL_free(pem_data); + + /* we got a pkey and an echconfig */ + if (pkey && cfg_data) + break; + } + + if (!cfg_data) { + memprintf(err, "%s'%s': no valid \"ECHCONFIG\" PEM block found", + err && *err ? *err : "", filename); + goto end; + } + if (priv_key_len == 0) { + memprintf(err, "%s'%s': no usable X25519 \"PRIVATE KEY\" PEM block found", + err && *err ? *err : "", filename); + goto end; + } + + if (!EVP_HPKE_KEY_init(&hpke_key, EVP_hpke_x25519_hkdf_sha256(), + priv_key, priv_key_len)) { + memprintf(err, "%s'%s': unable to initialize the HPKE key", + err && *err ? *err : "", filename); + goto end; + } + + if (!SSL_ECH_KEYS_add(store, 1 /* is_retry_config */, cfg_data, cfg_len, &hpke_key)) { + memprintf(err, "%s'%s': ECHConfig rejected by SSL_ECH_KEYS_add()", + err && *err ? *err : "", filename); + goto end; + } + + rv = 1; +end: + EVP_HPKE_KEY_cleanup(&hpke_key); + EVP_PKEY_free(pkey); + OPENSSL_free(cfg_data); + BIO_free_all(in); + return rv; +#else + BIO *in; + int rv; + + in = BIO_new_file(filename, "r"); + if (!in) { memprintf(err, "%sunable to open ECH key file '%s'", err && *err ? *err : "", filename); return 0; @@ -56,6 +163,7 @@ static int ech_store_load_file(ech_store *store, const char *filename, char **er err && *err ? *err : "", filename); BIO_free_all(in); return rv; +#endif } /* allocate a new, empty ech_store. @@ -63,14 +171,22 @@ static int ech_store_load_file(ech_store *store, const char *filename, char **er */ static ech_store *ech_store_new(void) { +#if defined(OPENSSL_IS_AWSLC) + return SSL_ECH_KEYS_new(); +#else return OSSL_ECHSTORE_new(NULL, NULL); +#endif } /* release . may be NULL. */ static void ech_store_free(ech_store *store) { +#if defined(OPENSSL_IS_AWSLC) + SSL_ECH_KEYS_free(store); +#else OSSL_ECHSTORE_free(store); +#endif } /* install as the active ECH configuration on . @@ -78,7 +194,11 @@ static void ech_store_free(ech_store *store) */ static int ech_store_set_ctx(SSL_CTX *ctx, ech_store *store) { +#if defined(OPENSSL_IS_AWSLC) + return SSL_CTX_set1_ech_keys(ctx, store) == 1; +#else return SSL_CTX_set1_echstore(ctx, store) == 1; +#endif } /* @@ -136,6 +256,9 @@ ignore_entry: err && *err ? *err : "", dirname); goto end; } +#if !defined(OPENSSL_IS_AWSLC) + /* only relevant with OpenSSL, ech_store_load_file() requires a private + * key in each file */ if (!OSSL_ECHSTORE_num_keys(es, loaded)) goto end; if (*loaded == 0) { @@ -143,6 +266,7 @@ ignore_entry: err && *err ? *err : "", dirname); goto end; } +#endif if (!ech_store_set_ctx(ctx, es)) goto end; rv = 1; @@ -165,6 +289,7 @@ end: return rv; } +#if !defined(OPENSSL_IS_AWSLC) /* find a named SSL_CTX, returns 1 if found * * should be in the format "frontend/@:" @@ -210,6 +335,7 @@ static int cli_find_ech_specific_ctx(const char *name, SSL_CTX **sctx) } return 0; } +#endif /* !OPENSSL_IS_AWSLC */ /* parsing function for 'show ssl ech [echfile]' */ static int cli_parse_show_ech(char **args, char *payload, @@ -220,6 +346,9 @@ static int cli_parse_show_ech(char **args, char *payload, if (!cli_has_level(appctx, ACCESS_LVL_ADMIN)) return 1; +#if defined(OPENSSL_IS_AWSLC) + return cli_err(appctx, "'show ssl ech' is not supported when built with AWS-LC\n"); +#else /* no parameter, shows only file list */ if (*args[3]) { @@ -241,10 +370,15 @@ static int cli_parse_show_ech(char **args, char *payload, } return 0; +#endif /* OPENSSL_IS_AWSLC */ } static void cli_print_ech_info(SSL_CTX *ctx, struct buffer *trash) { +#if defined(OPENSSL_IS_AWSLC) + return; +#else + int oi_ind, oi_cnt = 0; OSSL_ECHSTORE *es = NULL; BIO *out = NULL; @@ -293,6 +427,7 @@ end: BIO_free(out); OSSL_ECHSTORE_free(es); return; +#endif /* !OPENSSL_IS_AWSLC */ } /* @@ -377,6 +512,9 @@ end: /* add ssl ech */ static int cli_parse_add_ech(char **args, char *payload, struct appctx *appctx, void *private) { +#if defined(OPENSSL_IS_AWSLC) + return cli_err(appctx, "'add ssl ech' is not supported when built with AWS-LC\n"); +#else SSL_CTX *sctx = NULL; char success_message[ECH_SUCCESS_MSG_MAX]; OSSL_ECHSTORE *es = NULL; @@ -402,11 +540,15 @@ static int cli_parse_add_ech(char **args, char *payload, struct appctx *appctx, snprintf(success_message, ECH_SUCCESS_MSG_MAX, "added a new ECH config to %s", args[3]); return cli_msg(appctx, LOG_INFO, success_message); +#endif /* OPENSSL_IS_AWSLC */ } /* set ssl ech */ static int cli_parse_set_ech(char **args, char *payload, struct appctx *appctx, void *private) { +#if defined(OPENSSL_IS_AWSLC) + return cli_err(appctx, "'set ssl ech' is not supported when built with AWS-LC\n"); +#else SSL_CTX *sctx = NULL; char success_message[ECH_SUCCESS_MSG_MAX]; OSSL_ECHSTORE *es = NULL; @@ -432,11 +574,15 @@ static int cli_parse_set_ech(char **args, char *payload, struct appctx *appctx, snprintf(success_message, ECH_SUCCESS_MSG_MAX, "set new ECH configs for %s", args[3]); return cli_msg(appctx, LOG_INFO, success_message); +#endif /* OPENSSL_IS_AWSLC */ } /* del ssl ech [] */ static int cli_parse_del_ech(char **args, char *payload, struct appctx *appctx, void *private) { +#if defined(OPENSSL_IS_AWSLC) + return cli_err(appctx, "'del ssl ech' is not supported when built with AWS-LC\n"); +#else SSL_CTX *sctx = NULL; time_t age = 0; char success_message[ECH_SUCCESS_MSG_MAX]; @@ -466,6 +612,7 @@ static int cli_parse_del_ech(char **args, char *payload, struct appctx *appctx, snprintf(success_message, ECH_SUCCESS_MSG_MAX, "deleted ECH configs older than %ld seconds from %s", age, args[3]); return cli_msg(appctx, LOG_INFO, success_message); +#endif /* OPENSSL_IS_AWSLC */ } @@ -499,6 +646,9 @@ INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws); */ int conn_get_ech_status(struct connection *conn, struct buffer *buf) { +#if defined(OPENSSL_IS_AWSLC) + return 0; +#else struct ssl_sock_ctx *ctx = conn_get_ssl_sock_ctx(conn); char *sni_ech = NULL; char *sni_clr = NULL; @@ -522,11 +672,15 @@ int conn_get_ech_status(struct connection *conn, struct buffer *buf) OPENSSL_free(sni_ech); OPENSSL_free(sni_clr); return 1; +#endif /* OPENSSL_IS_AWSLC */ } /* If ECH succeeded, return the outer SNI value seen */ int conn_get_ech_outer_sni(struct connection *conn, struct buffer *buf) { +#if defined(OPENSSL_IS_AWSLC) + return 0; +#else struct ssl_sock_ctx *ctx = conn_get_ssl_sock_ctx(conn); char *sni_ech = NULL; char *sni_clr = NULL; @@ -539,6 +693,7 @@ int conn_get_ech_outer_sni(struct connection *conn, struct buffer *buf) OPENSSL_free(sni_ech); OPENSSL_free(sni_clr); return 1; +#endif /* OPENSSL_IS_AWSLC */ } static int bind_parse_ech(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err)