details: http://freenginx.org/hg/nginx/rev/deb1ec630f7c branches: changeset: 9422:deb1ec630f7c user: Maxim Dounin <[email protected]> date: Sat Sep 20 02:15:07 2025 +0300 description: 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. diffstat: src/event/ngx_event_openssl.c | 268 +++++++++++++++++++++++++++++++++ src/event/ngx_event_openssl.h | 5 + src/http/modules/ngx_http_ssl_module.c | 18 ++ src/http/modules/ngx_http_ssl_module.h | 2 + 4 files changed, 293 insertions(+), 0 deletions(-) diffs (354 lines): 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;
