[PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support

Maxim Dounin mdounin at mdounin.ru
Tue Sep 9 11:31:41 UTC 2025


# 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;



More information about the nginx-devel mailing list