[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