[PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support
Stephen Farrell
stephen.farrell at cs.tcd.ie
Tue Sep 9 11:55:56 UTC 2025
Hiya,
Great to see that. I'll give it a try in a day or two (travelling
at the moment). One initial question: the configuration directive
you added differs fron what we suggested for nginx and apache (we
suggested a directive names a directory of ECH PEM files), so I'm
wondering if there was a specific reason to take that approach?
Thanks,
Stephen
On 09/09/2025 12:31, Maxim Dounin wrote:
> # HG changeset patch
> # User Maxim Dounin <mdounin at mdounin.ru>
> # Date 1757416230 -10800
> # Tue Sep 09 14:10:30 2025 +0300
> # Node ID c28c012ef2a0448356ed0d8428bb373555689c8c
> # Parent 352c8eb2b67c869ebdbc40874ba3595fb2f06534
> SSL: Encrypted Client Hello (ECH) support.
>
> This change makes it possible to configure server support for TLS Encrypted
> Client Hello (https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni).
>
> The "ssl_encrypted_hello_key" directive specifies path to a PEM file with
> a private key and a ECH config list, as introduced by OpenSSL ECH feature
> branch (https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni).
>
> If multiple keys are specified, the first one (that is, the corresponding
> configuration) will be used for retries, and other keys are considered
> to be old or in mid-deployment.
>
> Both OpenSSL (ECH feature branch) and BoringSSL are supported.
>
> diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
> --- a/src/event/ngx_event_openssl.c
> +++ b/src/event/ngx_event_openssl.c
> @@ -1622,6 +1622,274 @@ ngx_ssl_early_data(ngx_conf_t *cf, ngx_s
>
>
> ngx_int_t
> +ngx_ssl_encrypted_hello_keys(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *paths)
> +{
> + if (paths == NULL) {
> + return NGX_OK;
> + }
> +
> +#ifdef OSSL_ECH_FOR_RETRY
> + {
> + BIO *bio;
> + EVP_PKEY *pkey;
> + ngx_str_t *path;
> + ngx_uint_t i;
> + OSSL_ECHSTORE *store;
> +
> + /* OpenSSL */
> +
> + store = OSSL_ECHSTORE_new(NULL, NULL);
> + if (store == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "OSSL_ECHSTORE_new() failed");
> + return NGX_ERROR;
> + }
> +
> + bio = NULL;
> + pkey = NULL;
> +
> + path = paths->elts;
> + for (i = 0; i < paths->nelts; i++) {
> +
> + if (ngx_conf_full_name(cf->cycle, &path[i], 1) != NGX_OK) {
> + goto failed;
> + }
> +
> + bio = BIO_new_file((char *) path[i].data, "r");
> + if (bio == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "BIO_new_file(\"%s\") failed", path[i].data);
> + goto failed;
> + }
> +
> + /*
> + * PEM file with PKCS#8 PrivateKey followed by ECHConfigList,
> + * https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni
> + *
> + * Since OSSL_ECHSTORE_read_pem() does not require a private key
> + * to be present, we instead use PEM_read_bio_PrivateKey() followed
> + * by OSSL_ECHSTORE_set1_key_and_read_pem().
> + */
> +
> + pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
> + if (pkey == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "PEM_read_bio_PrivateKey(\"%s\") failed",
> + path[i].data);
> + goto failed;
> + }
> +
> + if (OSSL_ECHSTORE_set1_key_and_read_pem(store, pkey, bio,
> + i == 0 ? OSSL_ECH_FOR_RETRY
> + : OSSL_ECH_NO_RETRY)
> + != 1)
> + {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "OSSL_ECHSTORE_set1_key_and_read_pem(\"%s\") failed",
> + path[i].data);
> + goto failed;
> + }
> +
> + EVP_PKEY_free(pkey);
> + pkey = NULL;
> +
> + BIO_free(bio);
> + bio = NULL;
> + }
> +
> + if (SSL_CTX_set1_echstore(ssl->ctx, store) != 1) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "SSL_CTX_set1_echstore() failed");
> + goto failed;
> + }
> +
> + OSSL_ECHSTORE_free(store);
> +
> + return NGX_OK;
> +
> +failed:
> +
> + OSSL_ECHSTORE_free(store);
> +
> + if (bio) {
> + BIO_free(bio);
> + }
> +
> + if (pkey) {
> + EVP_PKEY_free(pkey);
> + }
> +
> + return NGX_ERROR;
> +
> + }
> +#elif defined SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG
> + {
> + BIO *bio;
> + long configlen;
> + u_char *config, key[32];
> + size_t keylen;
> + EVP_PKEY *pkey;
> + ngx_str_t *path;
> + ngx_uint_t i;
> + SSL_ECH_KEYS *keys;
> + EVP_HPKE_KEY *hpkey;
> +
> + /* BoringSSL */
> +
> + keys = SSL_ECH_KEYS_new();
> + if (keys == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "SSL_ECH_KEYS_new() failed");
> + return NGX_ERROR;
> + }
> +
> + bio = NULL;
> + pkey = NULL;
> + config = NULL;
> + hpkey = NULL;
> +
> + path = paths->elts;
> + for (i = 0; i < paths->nelts; i++) {
> +
> + if (ngx_conf_full_name(cf->cycle, &path[i], 1) != NGX_OK) {
> + goto failed;
> + }
> +
> + bio = BIO_new_file((char *) path[i].data, "r");
> + if (bio == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "BIO_new_file(\"%s\") failed", path[i].data);
> + goto failed;
> + }
> +
> + /*
> + * PEM file with PKCS#8 PrivateKey followed by ECHConfigList,
> + * https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni
> + */
> +
> + pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
> + if (pkey == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "PEM_read_bio_PrivateKey(\"%s\") failed",
> + path[i].data);
> + goto failed;
> + }
> +
> + if (PEM_bytes_read_bio(&config, &configlen, NULL, "ECHCONFIG", bio,
> + NULL, NULL)
> + != 1)
> + {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "PEM_bytes_read_bio(\"%s\") failed",
> + path[i].data);
> + goto failed;
> + }
> +
> + /* Construct EVP_HPKE_KEY from private key */
> +
> + if (EVP_PKEY_id(pkey) != EVP_PKEY_X25519) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "EVP_PKEY_id(\"%s\") unsupported ECH key type, "
> + "only X25519 keys are supported on this platform",
> + path[i].data);
> + goto failed;
> + }
> +
> + keylen = 32;
> +
> + if (EVP_PKEY_get_raw_private_key(pkey, key, &keylen) != 1) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "EVP_PKEY_get_raw_private_key() failed");
> + goto failed;
> + }
> +
> + EVP_PKEY_free(pkey);
> + pkey = NULL;
> +
> + hpkey = EVP_HPKE_KEY_new();
> + if (hpkey == NULL) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "EVP_HPKE_KEY_new() failed");
> + }
> +
> + if (EVP_HPKE_KEY_init(hpkey, EVP_hpke_x25519_hkdf_sha256(),
> + key, keylen) != 1)
> + {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "EVP_HPKE_KEY_init() failed");
> + goto failed;
> + }
> +
> + /*
> + * PEM file contains ECHConfigList, whereas SSL_ECH_KEYS_add()
> + * expects ECHConfig, without the 2-byte length prefix
> + */
> +
> + if (SSL_ECH_KEYS_add(keys, i == 0, config + 2, configlen - 2, hpkey)
> + != 1)
> + {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "SSL_ECH_KEYS_add() failed");
> + goto failed;
> + }
> +
> + EVP_HPKE_KEY_free(hpkey);
> + hpkey = NULL;
> +
> + OPENSSL_free(config);
> + config = NULL;
> +
> + BIO_free(bio);
> + bio = NULL;
> + }
> +
> + if (SSL_CTX_set1_ech_keys(ssl->ctx, keys) != 1) {
> + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
> + "SSL_CTX_set1_ech_keys() failed");
> + goto failed;
> + }
> +
> + SSL_ECH_KEYS_free(keys);
> +
> + ngx_explicit_memzero(&key, 32);
> +
> + return NGX_OK;
> +
> +failed:
> +
> + SSL_ECH_KEYS_free(keys);
> +
> + if (bio) {
> + BIO_free(bio);
> + }
> +
> + if (pkey) {
> + EVP_PKEY_free(pkey);
> + }
> +
> + if (config) {
> + OPENSSL_free(config);
> + }
> +
> + if (hpkey) {
> + EVP_HPKE_KEY_free(hpkey);
> + }
> +
> + ngx_explicit_memzero(&key, 32);
> +
> + return NGX_ERROR;
> +
> + }
> +#else
> + ngx_log_error(NGX_LOG_WARN, ssl->log, 0,
> + "\"ssl_encrypted_hello_key\" is not supported on this "
> + "platform, ignored");
> + return NGX_OK;
> +#endif
> +}
> +
> +
> +ngx_int_t
> ngx_ssl_conf_commands(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *commands)
> {
> if (commands == NULL) {
> diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h
> --- a/src/event/ngx_event_openssl.h
> +++ b/src/event/ngx_event_openssl.h
> @@ -39,6 +39,9 @@
> #include <openssl/rand.h>
> #include <openssl/x509.h>
> #include <openssl/x509v3.h>
> +#ifdef SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG
> +#include <openssl/hpke.h>
> +#endif
>
> #define NGX_SSL_NAME "OpenSSL"
>
> @@ -232,6 +235,8 @@ ngx_int_t ngx_ssl_dhparam(ngx_conf_t *cf
> ngx_int_t ngx_ssl_ecdh_curve(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *name);
> ngx_int_t ngx_ssl_early_data(ngx_conf_t *cf, ngx_ssl_t *ssl,
> ngx_uint_t enable);
> +ngx_int_t ngx_ssl_encrypted_hello_keys(ngx_conf_t *cf, ngx_ssl_t *ssl,
> + ngx_array_t *paths);
> ngx_int_t ngx_ssl_conf_commands(ngx_conf_t *cf, ngx_ssl_t *ssl,
> ngx_array_t *commands);
>
> diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c
> --- a/src/http/modules/ngx_http_ssl_module.c
> +++ b/src/http/modules/ngx_http_ssl_module.c
> @@ -276,6 +276,13 @@ static ngx_command_t ngx_http_ssl_comma
> offsetof(ngx_http_ssl_srv_conf_t, early_data),
> NULL },
>
> + { ngx_string("ssl_encrypted_hello_key"),
> + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
> + ngx_conf_set_str_array_slot,
> + NGX_HTTP_SRV_CONF_OFFSET,
> + offsetof(ngx_http_ssl_srv_conf_t, encrypted_hello_keys),
> + NULL },
> +
> { ngx_string("ssl_conf_command"),
> NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE2,
> ngx_conf_set_keyval_slot,
> @@ -632,6 +639,7 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t
> sscf->ocsp_cache_zone = NGX_CONF_UNSET_PTR;
> sscf->stapling = NGX_CONF_UNSET;
> sscf->stapling_verify = NGX_CONF_UNSET;
> + sscf->encrypted_hello_keys = NGX_CONF_UNSET_PTR;
>
> return sscf;
> }
> @@ -889,6 +897,16 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *
> return NGX_CONF_ERROR;
> }
>
> + ngx_conf_merge_ptr_value(conf->encrypted_hello_keys,
> + prev->encrypted_hello_keys, NULL);
> +
> + if (ngx_ssl_encrypted_hello_keys(cf, &conf->ssl,
> + conf->encrypted_hello_keys)
> + != NGX_OK)
> + {
> + return NGX_CONF_ERROR;
> + }
> +
> if (ngx_ssl_conf_commands(cf, &conf->ssl, conf->conf_commands) != NGX_OK) {
> return NGX_CONF_ERROR;
> }
> diff --git a/src/http/modules/ngx_http_ssl_module.h b/src/http/modules/ngx_http_ssl_module.h
> --- a/src/http/modules/ngx_http_ssl_module.h
> +++ b/src/http/modules/ngx_http_ssl_module.h
> @@ -54,6 +54,8 @@ typedef struct {
> ngx_flag_t session_tickets;
> ngx_array_t *session_ticket_keys;
>
> + ngx_array_t *encrypted_hello_keys;
> +
> ngx_uint_t ocsp;
> ngx_str_t ocsp_responder;
> ngx_shm_zone_t *ocsp_cache_zone;
>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: OpenPGP_signature.asc
Type: application/pgp-signature
Size: 236 bytes
Desc: OpenPGP digital signature
URL: <http://freenginx.org/pipermail/nginx-devel/attachments/20250909/08327ee7/attachment.sig>
More information about the nginx-devel
mailing list