[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