From mdounin at mdounin.ru Mon Sep 1 01:20:52 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Mon, 1 Sep 2025 04:20:52 +0300 Subject: [nginx] Update mime-types In-Reply-To: <13eaac41ace2c393f047d5fd6ec0b10f@elven.pw> References: <6fd7b4aae9f284157bcd51b2eb936b82@elven.pw> <33cf12d6a7440f5538c60d38ff208870@elven.pw> <25057fabce20e04e31c2db1426b7606f@elven.pw> <13eaac41ace2c393f047d5fd6ec0b10f@elven.pw> Message-ID: Hello! On Mon, Aug 25, 2025 at 10:30:42PM +0300, Lafiel via nginx-devel wrote: > Maxim Dounin ?????(?) 2025-07-28 13:54: > > Hello! > > > > Does it make sense to use these extensions nowadays? > > > > If yes, how the consumers of these formats will react to the > > change - that is, is the "application/vnd.palm" type actually > > supported, or it's just a planned change which never happened > > in practice? > > > > Note that Apache uses "application/x-mobipocket-ebook" for > > "prc" (which is a prc-based ebook format), which suggests that > > "application/vnd.palm" might not work well at least for some > > consumers. > > > > If no, should it be removed instead? > > > > Also note that at least one common use of the "pdb" extension > > is a debug data for Microsoft C++ Compiler, and it's actively > > used in practice. > > I agree, some types are rarely used now. Then maybe it should be > removed? That's the question. The answer depends on many factors, and in general requires a separate research. > There are other rarely used types. Example: > $ awk '/^mml,/||/^jad,/||/^wml,/||/^jar,/||/^war,/||/^ear,/||/^hqx,/|| \ > /^odg,/||/^odp,/||/^ods,/||/^odt,/||/^wmlc,/||/^wasm,/||/^cco,/|| \ > /^jardiff,/||/^jnlp,/||/^sea,/||/^sit,/||/^xpi,/||/^xspf,/|| \ > /^ra,/||/^3gp,/||/^asf,/' ~/httparchive_exts.csv > 3gp,35,75 > wml,11,63 > odt,16,23 > jar,5,12 > wasm,5,9 > sea,2,8 > xspf,4,7 > asf,4,6 > odg,2,3 > ra,3,3 > war,1,1 > jnlp,1,1 > cco,1,1 > hqx,1,1 > ods,1,1 In general, the threshold for removing types should be much lower than the threshold for adding new types, since this might negatively affect existing uses, breaking POLA. Additionally, the particular examples demonstrate that httparchive stats shouldn't be the only factor to consider: for example, "wasm" is a perfectly active and only recently introduced extension for Web Assembly, while "wml" is a dead format for WAP sites, no longer used in practice. The fact that "wml" is apparently used more than "wasm" shows that the data might not be a good indicator of relative usage, at least in the area where the usage is low. Also, the "odg", "odp", "ods", and "odt" are extensions for OpenOffice document formats. Such files aren't often used for sites itself, but might be available for download, which makes them important for mime.types, but unlikely to appear with high numbers in httparchive data. I believe we've previously discussed this in this thread, here: https://freenginx.org/pipermail/nginx-devel/2024-March/000099.html In either case, removing of any type certainly needs a detailed research, demonstrating that the type is no longer used in practice. > > Numbers as I see them suggests that all these extensions are > > rather not used in practice, at least compared to "ts". > > > > Further, I would argue that "tsv" is a common extension for tab > > separated values format, and both Python and Apache map it to > > "text/tab-separated-values". > > > > I would rather refrain from this change. > > Maybe then just add the text/tab-separated-values type for .tsv files? > > $ awk '/^tsv,/' httparchive_exts.csv > tsv,174,326 That would be something I would rather accept if requested. On the other hand, tab separated values are most likely fine with any default type, such as "text/plain" or "application/binary", and this explains lack of requests to add this type and extension. > > See here about m4v/m4a as discussed earlier in this thread: > > > > https://freenginx.org/pipermail/nginx-devel/2024-April/000141.html > > > > Note well that this patch doesn't remove corresponding > > "video/x-m4v" type with the "m4v" extension, which makes the > > change essentially a nop (apart from producing a warning at > > startup). > > Forgot to delete video/x-m4v. > > If there were any problems, then Apache would not have added > the `audio/mp4` type for .m4a and mp4a files. As previously explained, using "audio/mp4" and "video/mp4" for "m4a" and "m4v" files is incorrect, as these are distinct types. As such, the question is what are the reasons for the change. Just for the record, the Apple site does not seem to be distinctive about what types they expect to be used. In particular, I was able to find the following pages with MIME type recommendations for m4v/m4a files on developer.apple.com: https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/AirPlayGuide/PreparingYourMediaforAirPlay/PreparingYourMediaforAirPlay.html https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingVideoforSafarioniPhone/CreatingVideoforSafarioniPhone.html https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html The first one, last modified in 2012, suggests to use "audio/mpeg4" and "video/mpeg4". Two others, last modified in 2016, suggests to use "audio/x-m4a" and "video/x-m4v". I personally tend to think, especially given the last link from the above, which contains a number of various types, including "audio/x-m4b" for "m4b" and "audio/x-m4p" for "m4p", that "audio/x-m4a" and "video/x-m4v" should be preferred. Still, I'm open to changes if some compelling reasons are provided. > > The numbers suggests that these extensions are not used in > > practice. > > > > And at least "movie" is a way too generic extension seen being > > used with other MIME types, notably "video/x-sgi-movie" in Apache > > and Python. > > > > We probably can consider adding just the "qt", but given the > > numbers I'm not sure it worth the effort. > > Apache also has such an extension, I think it's worth adding. > I can fix the patch later. > > $ awk '/video\/quicktime/' mime.types > video/quicktime qt mov I don't have any strong objections. Still, I'm not really convinced this extension is used in practice, and if it is, it is mostly used for QuickTime videos, and not for other purposes. > > At least Apache still uses "video/x-ms-asf". And the change > > looses details about the purpose of the file format, which might > > be the reason why "video/x-ms-asf" is still used. > > > > Any specific reasons for the change? > > According to this recommendation: > https://learn.microsoft.com/en-us/windows/win32/wmformat/file-name-extension-guidelines > asf files should not store audio and video streams. > This format is also rarely used, can it be removed as well? > This also applies to the .wmv extension. Then there will be > no need to add the .wma extension. As suggested above, threshold for removing extensions which are already present is expected to be much lower than that for adding new extensions. And at least "wmv" files are still widely used in practice, as far as I can tell, e.g., from various video sharing sites. [...] -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Mon Sep 1 08:27:25 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Mon, 01 Sep 2025 11:27:25 +0300 Subject: [PATCH] Tests: adjusted delays in ssl_session_ticket_key.t Message-ID: <468102e2a704618f2829.1756715245@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1756715195 -10800 # Mon Sep 01 11:26:35 2025 +0300 # Node ID 468102e2a704618f2829e68d340b6feffab609aa # Parent 25acbb7405e84b60b13f3894768d6fa778149ac7 Tests: adjusted delays in ssl_session_ticket_key.t. If session ticket keys are not shared among worker processes, waiting for 0.5 seconds before trying to establish another session won't change anything (if the connection will end up in the different worker process, there will be an error). At the same time, the test was seen unexpectedly failing with Valgrind, due to slow connection handling. Since session ticket key rotation uses time in seconds, slightly more than 1 second between requests is enough for the test to fail if the first request happens at the end of a second. Removing the unneeded sleep improves things, but not enough to fully resolve the issue. As such, session timeout was set to 3 seconds. Wait time between tests adjusted accordingly, to slightly more than 4 seconds, to ensure that the key will be changed even if the first request happens at the start of a second. diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -52,7 +52,7 @@ http { server_name localhost; ssl_session_cache shared:SSL:1m; - ssl_session_timeout 2; + ssl_session_timeout 3; } } @@ -92,10 +92,9 @@ foreach my $name ('localhost') { my $key = get_ticket_key_name(); -select undef, undef, undef, 0.5; is(get_ticket_key_name(), $key, 'ticket key match'); -select undef, undef, undef, 2.5; +select undef, undef, undef, 4.1; local $TODO = 'no ticket key callback' if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); From mdounin at mdounin.ru Fri Sep 5 21:13:19 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 06 Sep 2025 00:13:19 +0300 Subject: [nginx-tests] Tests: adjusted delays in ssl_session_ticket_key.t. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/468102e2a704 branches: changeset: 2024:468102e2a704 user: Maxim Dounin date: Mon Sep 01 11:26:35 2025 +0300 description: Tests: adjusted delays in ssl_session_ticket_key.t. If session ticket keys are not shared among worker processes, waiting for 0.5 seconds before trying to establish another session won't change anything (if the connection will end up in the different worker process, there will be an error). At the same time, the test was seen unexpectedly failing with Valgrind, due to slow connection handling. Since session ticket key rotation uses time in seconds, slightly more than 1 second between requests is enough for the test to fail if the first request happens at the end of a second. Removing the unneeded sleep improves things, but not enough to fully resolve the issue. As such, session timeout was set to 3 seconds. Wait time between tests adjusted accordingly, to slightly more than 4 seconds, to ensure that the key will be changed even if the first request happens at the start of a second. diffstat: ssl_session_ticket_key.t | 5 ++--- 1 files changed, 2 insertions(+), 3 deletions(-) diffs (24 lines): diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -52,7 +52,7 @@ http { server_name localhost; ssl_session_cache shared:SSL:1m; - ssl_session_timeout 2; + ssl_session_timeout 3; } } @@ -92,10 +92,9 @@ foreach my $name ('localhost') { my $key = get_ticket_key_name(); -select undef, undef, undef, 0.5; is(get_ticket_key_name(), $key, 'ticket key match'); -select undef, undef, undef, 2.5; +select undef, undef, undef, 4.1; local $TODO = 'no ticket key callback' if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); From mdounin at mdounin.ru Sat Sep 6 11:49:55 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 06 Sep 2025 14:49:55 +0300 Subject: [PATCH] Tests: removed stale comment, missed in 1937:2a0a6035a1af Message-ID: <009ff3a25affe30f3db4.1757159395@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1757159373 -10800 # Sat Sep 06 14:49:33 2025 +0300 # Node ID 009ff3a25affe30f3db45757366b0bd726f9bf21 # Parent 468102e2a704618f2829e68d340b6feffab609aa Tests: removed stale comment, missed in 1937:2a0a6035a1af. diff --git a/mail_imap.t b/mail_imap.t --- a/mail_imap.t +++ b/mail_imap.t @@ -258,10 +258,6 @@ local $TODO = 'not yet' unless $t->has_v $s->check(qr/^a03 BAD/, 'invalid with non-sync literal'); $s->check(qr/^(a04 |$)/, 'literal not command'); -# skipped without a fix, since with level-triggered event methods -# this hogs cpu till the connection is closed by the backend server, -# and generates a lot of debug logs - $s = Test::Nginx::IMAP->new(); $s->read(); From mdounin at mdounin.ru Tue Sep 9 11:02:48 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 09 Sep 2025 14:02:48 +0300 Subject: [nginx-tests] Tests: removed stale comment, missed in 1937:2a0a6... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/009ff3a25aff branches: changeset: 2025:009ff3a25aff user: Maxim Dounin date: Sat Sep 06 14:49:33 2025 +0300 description: Tests: removed stale comment, missed in 1937:2a0a6035a1af. diffstat: mail_imap.t | 4 ---- 1 files changed, 0 insertions(+), 4 deletions(-) diffs (14 lines): diff --git a/mail_imap.t b/mail_imap.t --- a/mail_imap.t +++ b/mail_imap.t @@ -258,10 +258,6 @@ local $TODO = 'not yet' unless $t->has_v $s->check(qr/^a03 BAD/, 'invalid with non-sync literal'); $s->check(qr/^(a04 |$)/, 'literal not command'); -# skipped without a fix, since with level-triggered event methods -# this hogs cpu till the connection is closed by the backend server, -# and generates a lot of debug logs - $s = Test::Nginx::IMAP->new(); $s->read(); From mdounin at mdounin.ru Tue Sep 9 11:31:41 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 09 Sep 2025 14:31:41 +0300 Subject: [PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support Message-ID: # HG changeset patch # User Maxim Dounin # 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 #include #include +#ifdef SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG +#include +#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; From mdounin at mdounin.ru Tue Sep 9 11:31:42 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 09 Sep 2025 14:31:42 +0300 Subject: [PATCH 2 of 2] SSL: $ssl_encrypted_hello variable In-Reply-To: References: Message-ID: <223d802d990cf5b32517.1757417502@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1757416233 -10800 # Tue Sep 09 14:10:33 2025 +0300 # Node ID 223d802d990cf5b32517fca34da299b243f37086 # Parent c28c012ef2a0448356ed0d8428bb373555689c8c SSL: $ssl_encrypted_hello variable. 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 @@ -5835,6 +5835,48 @@ ngx_ssl_get_early_data(ngx_connection_t ngx_int_t +ngx_ssl_get_encrypted_hello(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) +{ + s->len = 0; + +#ifdef OSSL_ECH_FOR_RETRY + { + char *outer, *inner; + + /* OpenSSL */ + + outer = NULL; + inner = NULL; + + if (SSL_ech_get1_status(c->ssl->connection, &outer, &inner) + == SSL_ECH_STATUS_SUCCESS) + { + ngx_str_set(s, "1"); + } + + if (outer) { + OPENSSL_free(outer); + } + + if (inner) { + OPENSSL_free(inner); + } + } +#elif defined SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG + + /* BoringSSL */ + + if (SSL_ech_accepted(c->ssl->connection)) { + ngx_str_set(s, "1"); + } + +#endif + + return NGX_OK; +} + + +ngx_int_t ngx_ssl_get_server_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME 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 @@ -289,6 +289,8 @@ ngx_int_t ngx_ssl_get_session_reused(ngx ngx_str_t *s); ngx_int_t ngx_ssl_get_early_data(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); +ngx_int_t ngx_ssl_get_encrypted_hello(ngx_connection_t *c, ngx_pool_t *pool, + ngx_str_t *s); ngx_int_t ngx_ssl_get_server_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_alpn_protocol(ngx_connection_t *c, ngx_pool_t *pool, 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 @@ -359,6 +359,9 @@ static ngx_http_variable_t ngx_http_ssl (uintptr_t) ngx_ssl_get_early_data, NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_NOCACHEABLE, 0 }, + { ngx_string("ssl_encrypted_hello"), NULL, ngx_http_ssl_variable, + (uintptr_t) ngx_ssl_get_encrypted_hello, NGX_HTTP_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_server_name"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_server_name, NGX_HTTP_VAR_CHANGEABLE, 0 }, From mdounin at mdounin.ru Tue Sep 9 11:33:08 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 09 Sep 2025 14:33:08 +0300 Subject: [PATCH] Tests: added tests for Encrypted Client Hello (ECH) In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1757416239 -10800 # Tue Sep 09 14:10:39 2025 +0300 # Node ID f1f43bdcf99ecadee593ced18f5bb3e2570f31ff # Parent 009ff3a25affe30f3db45757366b0bd726f9bf21 Tests: added tests for Encrypted Client Hello (ECH). diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t new file mode 100644 --- /dev/null +++ b/ssl_encrypted_hello.t @@ -0,0 +1,420 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for http ssl module, support for Encrypted Client Hello (ECH). + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl sni rewrite/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8443 ssl; + server_name public; + + ssl_certificate public.crt; + ssl_certificate_key public.key; + + ssl_encrypted_hello_key public.ech; + + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; + } + + server { + listen 127.0.0.1:8443 ssl; + server_name secret; + + ssl_certificate secret.crt; + ssl_certificate_key secret.key; + + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; + } + + server { + listen 127.0.0.1:8443 ssl; + server_name verify; + + ssl_certificate verify.crt; + ssl_certificate_key verify.key; + + ssl_verify_client optional_no_ca; + ssl_client_certificate verify.crt; + + return 200 "$ssl_server_name:$ssl_encrypted_hello:$ssl_client_verify\n"; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('public', 'secret', 'verify') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->write_file( + 'trusted.crt', + $t->read_file('public.crt') + . $t->read_file('secret.crt') + . $t->read_file('verify.crt') +); + +if ((`openssl ech -help 2>&1` || '') =~ m/-public_name/) { + + # Generate ECH file with "openssl ech" + + system('openssl ech ' + . "-out $d/public.ech " + . "-public_name public " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ECH config: $!\n"; + +} elsif ((`bssl 2>&1` || '') =~ m/generate-ech/) { + + # Generate ECH file with "bssl generate-ech" + # and additional manual formatting to produce a PEM file + + system('bssl generate-ech ' + . "-out-ech-config $d/public.echconfig.bin " + . "-out-ech-config-list $d/public.echconfiglist.bin " + . "-out-private-key $d/public.echkey.bin " + . "-public-name public " + . "-config-id 0 " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ECH config: $!\n"; + + my $list = $t->read_file('public.echconfiglist.bin'); + my $key = $t->read_file('public.echkey.bin'); + + # BoringSSL uses raw X25519 private key. Convert it to PKCS#8 + # PrivateKeyInfo. + + $key = "\x30\x2E" # SEQUENCE, 46 bytes + . "\x02\x01\x00" # INTEGER, 1 byte, 0 + . "\x30\x05" # SEQUENCE, 5 bytes + . "\x06\x03\x2B\x65\x6E" # OBJECT, 3 bytes, X25519 + . "\x04\x22" # OCTET STRING, 34 bytes + . "\x04\x20" # OCTET STRING, 32 bytes + . $key; + + $t->write_file( + 'public.ech', + "-----BEGIN PRIVATE KEY-----\n" + . encode_base64($key) + . "-----END PRIVATE KEY-----\n" + . "-----BEGIN ECHCONFIG-----\n" + . encode_base64($list) + . "-----END ECHCONFIG-----\n" + ); + +} else { + plan(skip_all => 'no openssl ech or bssl generate-ech') +} + +$t->try_run('no ssl_encrypted_hello_key')->plan(8); + +############################################################################### + +my ($cmd, $req, $out); +my $port = port(8443); + +# ECH file looks like: +# +# -----BEGIN PRIVATE KEY----- +# MC4CAQAwBQYDK2VuBCIEIMhvGkKTR2gchVcurYDocK4v1Y5wac20UZzB3JB0QMVh +# -----END PRIVATE KEY----- +# -----BEGIN ECHCONFIG----- +# AEX+DQBBGwAgACC2q1Z7YDL1X4bahRyJeBZb3bwHPITBUxqFBS2CIfXCGQAEAAEA +# AQAScHVibGljLmV4YW1wbGUub3JnAAA= +# -----END ECHCONFIG----- +# +# To use on the client we need ECHCONFIG part, which contains ECHConfigList +# structure. + +my $config = $t->read_file('public.ech'); +$config =~ s/.*-----BEGIN ECHCONFIG-----(.*)-----END.*/$1/s; +$config =~ s/[\n\r\s]//g; + +# Requests to use + +$t->write_file('req-secret', "GET / HTTP/1.0\nHost: secret\n\n"); +$t->write_file('req-verify', "GET / HTTP/1.0\nHost: verify\n\n"); + +SKIP: { +skip 'no openssl client ech', 4 + if `openssl s_client -help 2>&1` !~ /-ech_config_list/; + +# Tests with OpenSSL s_client from ECH feature branch + +# Note that OpenSSL s_client prints confusing "ECH: BAD NAME: -102" status +# when it is not able to verify server certificate. To make sure proper +# success is visible in the output, we therefore explicitly provide trusted +# root certificates. + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername secret " + . "-ech_config_list $config " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +# Note that OpenSSL s_client from ECH feature branch currently cannot talk +# to a server with BoringSSL. BoringSSL error on the server is as follows: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines: +# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines: +# OPENSSL_internal:DECRYPTION_FAILED)... + +TODO: { +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' + if $t->has_module('BoringSSL'); +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +like($out, qr/^ECH: success.*secret:1$/ms, 'openssl client'); + +} + +# Test without ECH, to make sure the $ssl_encrypted_hello variable +# is properly set. +# +# The test explicitly requests @SECLEVEL=0 for libraries without TLSv1.2 +# support, such as OpenSSL 1.0.0. + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername secret " + . "-cipher DEFAULT:\@SECLEVEL=0 " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: NOT CONFIGURED.*secret:$/ms, 'openssl client no ech'); + +# Tests with client certificate verification, +# mostly to check if the $ssl_encrypted_hello variable is correct, notably +# with failed client certificate verification. +# +# Currently fails with OpenSSL ECH feature branch on server, the error is +# as follows: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: +# missing fatal)... +# +# Also, similarly to the above, this fails with BoringSSL on the server. + +TODO: { +local $TODO = 'OpenSSL broken verify' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && $t->has_feature('openssl:3.6.0'); +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' + if $t->has_module('BoringSSL'); +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername verify " + . "-ech_config_list $config " + . "-cert $d/verify.crt " + . "-key $d/verify.key " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: success.*verify:1:SUCCESS/ms, 'openssl client verify'); + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername verify " + . "-ech_config_list $config " + . "-cert $d/secret.crt " + . "-key $d/secret.key " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: success.*verify:1:FAILED/ms, + 'openssl client verify failed'); + +} +} + +SKIP: { +skip 'no bssl client ech', 4 + if (`bssl client -help 2>&1` || '') !~ /-ech-config-list/; + +# Tests with BoringSSL bssl tool + +# BoringSSL bssl tool uses a file with binary ECHConfigList +# representation. + +$t->write_file('public.bin', decode_base64($config)); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name secret " + . "-ech-config-list $d/public.bin " + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +TODO: { +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +like($out, qr/Encrypted ClientHello: yes.*secret:1$/ms, 'bssl client'); + +} + +# Test without ECH, to make sure the $ssl_encrypted_hello variable +# is properly set. +# +# The test explicitly requests TLSv1.0 for libraries without TLSv1.2 +# support, such as OpenSSL 1.0.0. + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name secret " + . "-min-version tls1 " + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: no.*secret:$/ms, 'bssl client no ech'); + +# Tests with client certificate verification, +# mostly to check if the $ssl_encrypted_hello variable is correct, notably +# with failed client certificate verification. +# +# Currently fails with OpenSSL ECH feature branch on server, the error is +# as follows: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: +# missing fatal)... + +TODO: { +local $TODO = 'OpenSSL broken verify' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && $t->has_feature('openssl:3.6.0'); +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name verify " + . "-ech-config-list $d/public.bin " + . "-cert $d/verify.crt " + . "-key $d/verify.key " + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: yes.*verify:1:SUCCESS/ms, + 'bssl client verify'); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name verify " + . "-ech-config-list $d/public.bin " + . "-cert $d/secret.crt " + . "-key $d/secret.key " + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: yes.*verify:1:FAILED/ms, + 'bssl client verify failed'); + +} +} + +############################################################################### From stephen.farrell at cs.tcd.ie Tue Sep 9 11:55:56 2025 From: stephen.farrell at cs.tcd.ie (Stephen Farrell) Date: Tue, 9 Sep 2025 12:55:56 +0100 Subject: [PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support In-Reply-To: References: Message-ID: <841bd64a-be98-4c72-be2f-049e4b09761b@cs.tcd.ie> 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 > # 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 > #include > #include > +#ifdef SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG > +#include > +#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: From mdounin at mdounin.ru Tue Sep 9 14:18:23 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 9 Sep 2025 17:18:23 +0300 Subject: [PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support In-Reply-To: <841bd64a-be98-4c72-be2f-049e4b09761b@cs.tcd.ie> References: <841bd64a-be98-4c72-be2f-049e4b09761b@cs.tcd.ie> Message-ID: Hello! On Tue, Sep 09, 2025 at 12:55:56PM +0100, Stephen Farrell wrote: > 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? I personally don't think that a directory with files is a great approach to managing keys, and rather prefer explicit specification of which files to use as keys instead. In particular, this approach makes it easier to check files provided and complain if there are any issues, such as accidental corruption. It also makes it possible to easily specify which key(s) should be used for retries (currently the first one specified; can be extended to an explicit flag if more than one retry key will be needed). Also, I don't think that more than 3 keys are actually needed here, even considering automatic rotation (that is, current key, next key, and previous key), and a directory for just 3 keys looks like an overkill to me. Last but not least, the approach with explicitly specified keys is in line with what [free]nginx already uses for TLS session ticket keys (http://freenginx.org/r/ssl_session_ticket_key). Using the same approach for Encrypted Client Hello keys is quite natural. I believe I've previously suggested to use this approach during early review of your ECH patches. [...] -- Maxim Dounin http://mdounin.ru/ From stephen.farrell at cs.tcd.ie Thu Sep 11 19:32:28 2025 From: stephen.farrell at cs.tcd.ie (Stephen Farrell) Date: Thu, 11 Sep 2025 20:32:28 +0100 Subject: [PATCH 1 of 2] SSL: Encrypted Client Hello (ECH) support In-Reply-To: <841bd64a-be98-4c72-be2f-049e4b09761b@cs.tcd.ie> References: <841bd64a-be98-4c72-be2f-049e4b09761b@cs.tcd.ie> Message-ID: Hiya, On 09/09/2025 12:55, Stephen Farrell wrote: > I'll?give?it?a?try?in?a?day?or?two No surprise give the source of the patch, but for the record, I built this locally and it worked fine in a basic test using the `openssl s_client` and curl. I'll follow up with some comments on the patches tomorrow. Cheers, S. -------------- next part -------------- A non-text attachment was scrubbed... Name: OpenPGP_signature.asc Type: application/pgp-signature Size: 236 bytes Desc: OpenPGP digital signature URL: From mdounin at mdounin.ru Tue Sep 16 22:08:59 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 17 Sep 2025 01:08:59 +0300 Subject: [PATCH 2 of 2] SSL: $ssl_encrypted_hello variable In-Reply-To: <223d802d990cf5b32517.1757417502@vm-bsd.mdounin.ru> References: <223d802d990cf5b32517.1757417502@vm-bsd.mdounin.ru> Message-ID: Hello! On Tue, Sep 09, 2025 at 02:31:42PM +0300, Maxim Dounin wrote: > # HG changeset patch > # User Maxim Dounin > # Date 1757416233 -10800 > # Tue Sep 09 14:10:33 2025 +0300 > # Node ID 223d802d990cf5b32517fca34da299b243f37086 > # Parent c28c012ef2a0448356ed0d8428bb373555689c8c > SSL: $ssl_encrypted_hello variable. > > 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 > @@ -5835,6 +5835,48 @@ ngx_ssl_get_early_data(ngx_connection_t > > > ngx_int_t > +ngx_ssl_get_encrypted_hello(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) > +{ > + s->len = 0; > + > +#ifdef OSSL_ECH_FOR_RETRY > + { > + char *outer, *inner; > + > + /* OpenSSL */ > + > + outer = NULL; > + inner = NULL; > + > + if (SSL_ech_get1_status(c->ssl->connection, &outer, &inner) > + == SSL_ECH_STATUS_SUCCESS) > + { > + ngx_str_set(s, "1"); > + } > + > + if (outer) { > + OPENSSL_free(outer); > + } > + > + if (inner) { > + OPENSSL_free(inner); > + } > + } With upcoming fixes to client certificate verification in OpenSSL ECH branch (https://github.com/openssl/openssl/pull/28555), this is adjusted as follows: 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 @@ -5841,6 +5841,7 @@ ngx_ssl_get_encrypted_hello(ngx_connecti #ifdef OSSL_ECH_FOR_RETRY { + int status; char *outer, *inner; /* OpenSSL */ @@ -5848,8 +5849,10 @@ ngx_ssl_get_encrypted_hello(ngx_connecti outer = NULL; inner = NULL; - if (SSL_ech_get1_status(c->ssl->connection, &outer, &inner) - == SSL_ECH_STATUS_SUCCESS) + status = SSL_ech_get1_status(c->ssl->connection, &outer, &inner); + + if (status == SSL_ECH_STATUS_SUCCESS + || status == SSL_ECH_STATUS_BAD_NAME) { ngx_str_set(s, "1"); } [...] -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed Sep 17 01:26:01 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 17 Sep 2025 04:26:01 +0300 Subject: [PATCH] Tests: added tests for Encrypted Client Hello (ECH) In-Reply-To: References: Message-ID: Hello! On Tue, Sep 09, 2025 at 02:33:08PM +0300, Maxim Dounin wrote: > # HG changeset patch > # User Maxim Dounin > # Date 1757416239 -10800 > # Tue Sep 09 14:10:39 2025 +0300 > # Node ID f1f43bdcf99ecadee593ced18f5bb3e2570f31ff > # Parent 009ff3a25affe30f3db45757366b0bd726f9bf21 > Tests: added tests for Encrypted Client Hello (ECH). > > diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t > new file mode 100644 > --- /dev/null > +++ b/ssl_encrypted_hello.t > @@ -0,0 +1,420 @@ > +#!/usr/bin/perl > + > +# (C) Maxim Dounin > + > +# Tests for http ssl module, support for Encrypted Client Hello (ECH). > + > +############################################################################### > + > +use warnings; > +use strict; > + > +use Test::More; > + > +use MIME::Base64; > + > +BEGIN { use FindBin; chdir($FindBin::Bin); } > + > +use lib 'lib'; > +use Test::Nginx; > + > +############################################################################### > + > +select STDERR; $| = 1; > +select STDOUT; $| = 1; > + > +my $t = Test::Nginx->new() > + ->has(qw/http http_ssl sni rewrite/) > + ->has_daemon('openssl'); > + > +$t->write_file_expand('nginx.conf', <<'EOF'); > + > +%%TEST_GLOBALS%% > + > +daemon off; > + > +events { > +} > + > +http { > + %%TEST_GLOBALS_HTTP%% > + > + server { > + listen 127.0.0.1:8443 ssl; > + server_name public; > + > + ssl_certificate public.crt; > + ssl_certificate_key public.key; > + > + ssl_encrypted_hello_key public.ech; > + > + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; > + } > + > + server { > + listen 127.0.0.1:8443 ssl; > + server_name secret; > + > + ssl_certificate secret.crt; > + ssl_certificate_key secret.key; > + > + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; > + } > + > + server { > + listen 127.0.0.1:8443 ssl; > + server_name verify; > + > + ssl_certificate verify.crt; > + ssl_certificate_key verify.key; > + > + ssl_verify_client optional_no_ca; > + ssl_client_certificate verify.crt; > + > + return 200 "$ssl_server_name:$ssl_encrypted_hello:$ssl_client_verify\n"; > + } > +} > + > +EOF > + > +$t->write_file('openssl.conf', < +[ req ] > +default_bits = 2048 > +encrypt_key = no > +distinguished_name = req_distinguished_name > +[ req_distinguished_name ] > +EOF > + > +my $d = $t->testdir(); > + > +foreach my $name ('public', 'secret', 'verify') { > + system('openssl req -x509 -new ' > + . "-config $d/openssl.conf -subj /CN=$name/ " > + . "-out $d/$name.crt -keyout $d/$name.key " > + . ">>$d/openssl.out 2>&1") == 0 > + or die "Can't create certificate for $name: $!\n"; > +} > + > +$t->write_file( > + 'trusted.crt', > + $t->read_file('public.crt') > + . $t->read_file('secret.crt') > + . $t->read_file('verify.crt') > +); > + > +if ((`openssl ech -help 2>&1` || '') =~ m/-public_name/) { > + > + # Generate ECH file with "openssl ech" > + > + system('openssl ech ' > + . "-out $d/public.ech " > + . "-public_name public " > + . ">>$d/openssl.out 2>&1") == 0 > + or die "Can't create ECH config: $!\n"; > + > +} elsif ((`bssl 2>&1` || '') =~ m/generate-ech/) { > + > + # Generate ECH file with "bssl generate-ech" > + # and additional manual formatting to produce a PEM file > + > + system('bssl generate-ech ' > + . "-out-ech-config $d/public.echconfig.bin " > + . "-out-ech-config-list $d/public.echconfiglist.bin " > + . "-out-private-key $d/public.echkey.bin " > + . "-public-name public " > + . "-config-id 0 " > + . ">>$d/openssl.out 2>&1") == 0 > + or die "Can't create ECH config: $!\n"; > + > + my $list = $t->read_file('public.echconfiglist.bin'); > + my $key = $t->read_file('public.echkey.bin'); > + > + # BoringSSL uses raw X25519 private key. Convert it to PKCS#8 > + # PrivateKeyInfo. > + > + $key = "\x30\x2E" # SEQUENCE, 46 bytes > + . "\x02\x01\x00" # INTEGER, 1 byte, 0 > + . "\x30\x05" # SEQUENCE, 5 bytes > + . "\x06\x03\x2B\x65\x6E" # OBJECT, 3 bytes, X25519 > + . "\x04\x22" # OCTET STRING, 34 bytes > + . "\x04\x20" # OCTET STRING, 32 bytes > + . $key; > + > + $t->write_file( > + 'public.ech', > + "-----BEGIN PRIVATE KEY-----\n" > + . encode_base64($key) > + . "-----END PRIVATE KEY-----\n" > + . "-----BEGIN ECHCONFIG-----\n" > + . encode_base64($list) > + . "-----END ECHCONFIG-----\n" > + ); > + > +} else { > + plan(skip_all => 'no openssl ech or bssl generate-ech') > +} > + > +$t->try_run('no ssl_encrypted_hello_key')->plan(8); > + > +############################################################################### > + > +my ($cmd, $req, $out); > +my $port = port(8443); > + > +# ECH file looks like: > +# > +# -----BEGIN PRIVATE KEY----- > +# MC4CAQAwBQYDK2VuBCIEIMhvGkKTR2gchVcurYDocK4v1Y5wac20UZzB3JB0QMVh > +# -----END PRIVATE KEY----- > +# -----BEGIN ECHCONFIG----- > +# AEX+DQBBGwAgACC2q1Z7YDL1X4bahRyJeBZb3bwHPITBUxqFBS2CIfXCGQAEAAEA > +# AQAScHVibGljLmV4YW1wbGUub3JnAAA= > +# -----END ECHCONFIG----- > +# > +# To use on the client we need ECHCONFIG part, which contains ECHConfigList > +# structure. > + > +my $config = $t->read_file('public.ech'); > +$config =~ s/.*-----BEGIN ECHCONFIG-----(.*)-----END.*/$1/s; > +$config =~ s/[\n\r\s]//g; > + > +# Requests to use > + > +$t->write_file('req-secret', "GET / HTTP/1.0\nHost: secret\n\n"); > +$t->write_file('req-verify', "GET / HTTP/1.0\nHost: verify\n\n"); > + > +SKIP: { > +skip 'no openssl client ech', 4 > + if `openssl s_client -help 2>&1` !~ /-ech_config_list/; > + > +# Tests with OpenSSL s_client from ECH feature branch > + > +# Note that OpenSSL s_client prints confusing "ECH: BAD NAME: -102" status > +# when it is not able to verify server certificate. To make sure proper > +# success is visible in the output, we therefore explicitly provide trusted > +# root certificates. > + > +$cmd = "openssl s_client " > + . "-connect 127.0.0.1:$port " > + . "-servername secret " > + . "-ech_config_list $config " > + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +# Note that OpenSSL s_client from ECH feature branch currently cannot talk > +# to a server with BoringSSL. BoringSSL error on the server is as follows: > +# > +# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines: > +# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines: > +# OPENSSL_internal:DECRYPTION_FAILED)... > + > +TODO: { > +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' > + if $t->has_module('BoringSSL'); > +local $TODO = 'OpenSSL too old' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && !$t->has_feature('openssl:3.6.0'); > +local $TODO = 'LibreSSL has no support yet' > + if $t->has_module('LibreSSL'); > + > +like($out, qr/^ECH: success.*secret:1$/ms, 'openssl client'); > + > +} > + > +# Test without ECH, to make sure the $ssl_encrypted_hello variable > +# is properly set. > +# > +# The test explicitly requests @SECLEVEL=0 for libraries without TLSv1.2 > +# support, such as OpenSSL 1.0.0. > + > +$cmd = "openssl s_client " > + . "-connect 127.0.0.1:$port " > + . "-servername secret " > + . "-cipher DEFAULT:\@SECLEVEL=0 " > + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/^ECH: NOT CONFIGURED.*secret:$/ms, 'openssl client no ech'); > + > +# Tests with client certificate verification, > +# mostly to check if the $ssl_encrypted_hello variable is correct, notably > +# with failed client certificate verification. > +# > +# Currently fails with OpenSSL ECH feature branch on server, the error is > +# as follows: > +# > +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: > +# missing fatal)... > +# > +# Also, similarly to the above, this fails with BoringSSL on the server. > + > +TODO: { > +local $TODO = 'OpenSSL broken verify' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && $t->has_feature('openssl:3.6.0'); > +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' > + if $t->has_module('BoringSSL'); > +local $TODO = 'OpenSSL too old' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && !$t->has_feature('openssl:3.6.0'); > +local $TODO = 'LibreSSL has no support yet' > + if $t->has_module('LibreSSL'); > + > +$cmd = "openssl s_client " > + . "-connect 127.0.0.1:$port " > + . "-servername verify " > + . "-ech_config_list $config " > + . "-cert $d/verify.crt " > + . "-key $d/verify.key " > + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/^ECH: success.*verify:1:SUCCESS/ms, 'openssl client verify'); > + > +$cmd = "openssl s_client " > + . "-connect 127.0.0.1:$port " > + . "-servername verify " > + . "-ech_config_list $config " > + . "-cert $d/secret.crt " > + . "-key $d/secret.key " > + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/^ECH: success.*verify:1:FAILED/ms, > + 'openssl client verify failed'); > + > +} > +} > + > +SKIP: { > +skip 'no bssl client ech', 4 > + if (`bssl client -help 2>&1` || '') !~ /-ech-config-list/; > + > +# Tests with BoringSSL bssl tool > + > +# BoringSSL bssl tool uses a file with binary ECHConfigList > +# representation. > + > +$t->write_file('public.bin', decode_base64($config)); > + > +$cmd = "bssl client " > + . "-connect 127.0.0.1:$port " > + . "-server-name secret " > + . "-ech-config-list $d/public.bin " > + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +TODO: { > +local $TODO = 'OpenSSL too old' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && !$t->has_feature('openssl:3.6.0'); > +local $TODO = 'LibreSSL has no support yet' > + if $t->has_module('LibreSSL'); > + > +like($out, qr/Encrypted ClientHello: yes.*secret:1$/ms, 'bssl client'); > + > +} > + > +# Test without ECH, to make sure the $ssl_encrypted_hello variable > +# is properly set. > +# > +# The test explicitly requests TLSv1.0 for libraries without TLSv1.2 > +# support, such as OpenSSL 1.0.0. > + > +$cmd = "bssl client " > + . "-connect 127.0.0.1:$port " > + . "-server-name secret " > + . "-min-version tls1 " > + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/Encrypted ClientHello: no.*secret:$/ms, 'bssl client no ech'); > + > +# Tests with client certificate verification, > +# mostly to check if the $ssl_encrypted_hello variable is correct, notably > +# with failed client certificate verification. > +# > +# Currently fails with OpenSSL ECH feature branch on server, the error is > +# as follows: > +# > +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: > +# missing fatal)... > + > +TODO: { > +local $TODO = 'OpenSSL broken verify' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && $t->has_feature('openssl:3.6.0'); > +local $TODO = 'OpenSSL too old' > + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') > + && !$t->has_feature('openssl:3.6.0'); > +local $TODO = 'LibreSSL has no support yet' > + if $t->has_module('LibreSSL'); > + > +$cmd = "bssl client " > + . "-connect 127.0.0.1:$port " > + . "-server-name verify " > + . "-ech-config-list $d/public.bin " > + . "-cert $d/verify.crt " > + . "-key $d/verify.key " > + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/Encrypted ClientHello: yes.*verify:1:SUCCESS/ms, > + 'bssl client verify'); > + > +$cmd = "bssl client " > + . "-connect 127.0.0.1:$port " > + . "-server-name verify " > + . "-ech-config-list $d/public.bin " > + . "-cert $d/secret.crt " > + . "-key $d/secret.key " > + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; > + > +log_out($cmd); > + > +$out = `$cmd`; > + > +log_in($out); > + > +like($out, qr/Encrypted ClientHello: yes.*verify:1:FAILED/ms, > + 'bssl client verify failed'); > + > +} > +} > + > +############################################################################### > And here are updates to the tests, following feedback from Stephen Farrell on the observed issues with OpenSSL ECH feature branch: diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t --- a/ssl_encrypted_hello.t +++ b/ssl_encrypted_hello.t @@ -193,11 +193,22 @@ skip 'no openssl client ech', 4 # when it is not able to verify server certificate. To make sure proper # success is visible in the output, we therefore explicitly provide trusted # root certificates. +# +# Further, with TLSv1.2 and older protocols enabled OpenSSL s_client currently +# creates incorrect inner ClientHello, which is rejected by BoringSSL with +# the following error on the server: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines: +# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines: +# OPENSSL_internal:DECRYPTION_FAILED)... +# +# As a workaround, we explicitly request TLSv1.3 only. $cmd = "openssl s_client " . "-connect 127.0.0.1:$port " . "-servername secret " . "-ech_config_list $config " + . "-tls1_3 " . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; log_out($cmd); @@ -206,16 +217,7 @@ log_out($cmd); log_in($out); -# Note that OpenSSL s_client from ECH feature branch currently cannot talk -# to a server with BoringSSL. BoringSSL error on the server is as follows: -# -# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines: -# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines: -# OPENSSL_internal:DECRYPTION_FAILED)... - TODO: { -local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' - if $t->has_module('BoringSSL'); local $TODO = 'OpenSSL too old' if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') && !$t->has_feature('openssl:3.6.0'); @@ -250,20 +252,19 @@ like($out, qr/^ECH: NOT CONFIGURED.*secr # mostly to check if the $ssl_encrypted_hello variable is correct, notably # with failed client certificate verification. # -# Currently fails with OpenSSL ECH feature branch on server, the error is -# as follows: +# Currently fails with OpenSSL ECH feature branch on the server, +# the error is as follows: # # ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: # missing fatal)... # -# Also, similarly to the above, this fails with BoringSSL on the server. +# This is expected to be fixed by +# https://github.com/openssl/openssl/pull/28555. TODO: { local $TODO = 'OpenSSL broken verify' if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') && $t->has_feature('openssl:3.6.0'); -local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL' - if $t->has_module('BoringSSL'); local $TODO = 'OpenSSL too old' if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') && !$t->has_feature('openssl:3.6.0'); @@ -276,6 +277,7 @@ local $TODO = 'LibreSSL has no support y . "-ech_config_list $config " . "-cert $d/verify.crt " . "-key $d/verify.key " + . "-tls1_3 " . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; log_out($cmd); @@ -292,6 +294,7 @@ like($out, qr/^ECH: success.*verify:1:SU . "-ech_config_list $config " . "-cert $d/secret.crt " . "-key $d/secret.key " + . "-tls1_3 " . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; log_out($cmd); @@ -363,12 +366,6 @@ like($out, qr/Encrypted ClientHello: no. # Tests with client certificate verification, # mostly to check if the $ssl_encrypted_hello variable is correct, notably # with failed client certificate verification. -# -# Currently fails with OpenSSL ECH feature branch on server, the error is -# as follows: -# -# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: -# missing fatal)... TODO: { local $TODO = 'OpenSSL broken verify' -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Sat Sep 20 00:46:50 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 20 Sep 2025 03:46:50 +0300 Subject: [nginx] Version bump. Message-ID: details: http://freenginx.org/hg/nginx/rev/bd4c917140c8 branches: changeset: 9421:bd4c917140c8 user: Maxim Dounin date: Sat Sep 20 02:15:01 2025 +0300 description: Version bump. diffstat: src/core/nginx.h | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diffs (14 lines): diff --git a/src/core/nginx.h b/src/core/nginx.h --- a/src/core/nginx.h +++ b/src/core/nginx.h @@ -9,8 +9,8 @@ #define _NGINX_H_INCLUDED_ -#define nginx_version 1029001 -#define NGINX_VERSION "1.29.1" +#define nginx_version 1029002 +#define NGINX_VERSION "1.29.2" #define freenginx 1 From mdounin at mdounin.ru Sat Sep 20 00:46:50 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 20 Sep 2025 03:46:50 +0300 Subject: [nginx] SSL: Encrypted Client Hello (ECH) support. Message-ID: details: http://freenginx.org/hg/nginx/rev/deb1ec630f7c branches: changeset: 9422:deb1ec630f7c user: Maxim Dounin 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 #include #include +#ifdef SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG +#include +#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; From mdounin at mdounin.ru Sat Sep 20 00:46:50 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 20 Sep 2025 03:46:50 +0300 Subject: [nginx] SSL: $ssl_encrypted_hello variable. Message-ID: details: http://freenginx.org/hg/nginx/rev/84f82b372bc0 branches: changeset: 9423:84f82b372bc0 user: Maxim Dounin date: Sat Sep 20 02:16:13 2025 +0300 description: SSL: $ssl_encrypted_hello variable. diffstat: src/event/ngx_event_openssl.c | 45 ++++++++++++++++++++++++++++++++++ src/event/ngx_event_openssl.h | 2 + src/http/modules/ngx_http_ssl_module.c | 3 ++ 3 files changed, 50 insertions(+), 0 deletions(-) diffs (80 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 @@ -5835,6 +5835,51 @@ ngx_ssl_get_early_data(ngx_connection_t ngx_int_t +ngx_ssl_get_encrypted_hello(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) +{ + s->len = 0; + +#ifdef OSSL_ECH_FOR_RETRY + { + int status; + char *outer, *inner; + + /* OpenSSL */ + + outer = NULL; + inner = NULL; + + status = SSL_ech_get1_status(c->ssl->connection, &outer, &inner); + + if (status == SSL_ECH_STATUS_SUCCESS + || status == SSL_ECH_STATUS_BAD_NAME) + { + ngx_str_set(s, "1"); + } + + if (outer) { + OPENSSL_free(outer); + } + + if (inner) { + OPENSSL_free(inner); + } + } +#elif defined SSL_R_UNSUPPORTED_ECH_SERVER_CONFIG + + /* BoringSSL */ + + if (SSL_ech_accepted(c->ssl->connection)) { + ngx_str_set(s, "1"); + } + +#endif + + return NGX_OK; +} + + +ngx_int_t ngx_ssl_get_server_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME 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 @@ -289,6 +289,8 @@ ngx_int_t ngx_ssl_get_session_reused(ngx ngx_str_t *s); ngx_int_t ngx_ssl_get_early_data(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); +ngx_int_t ngx_ssl_get_encrypted_hello(ngx_connection_t *c, ngx_pool_t *pool, + ngx_str_t *s); ngx_int_t ngx_ssl_get_server_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_alpn_protocol(ngx_connection_t *c, ngx_pool_t *pool, 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 @@ -359,6 +359,9 @@ static ngx_http_variable_t ngx_http_ssl (uintptr_t) ngx_ssl_get_early_data, NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_NOCACHEABLE, 0 }, + { ngx_string("ssl_encrypted_hello"), NULL, ngx_http_ssl_variable, + (uintptr_t) ngx_ssl_get_encrypted_hello, NGX_HTTP_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_server_name"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_server_name, NGX_HTTP_VAR_CHANGEABLE, 0 }, From mdounin at mdounin.ru Sat Sep 20 00:46:58 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 20 Sep 2025 03:46:58 +0300 Subject: [nginx-tests] Tests: added tests for Encrypted Client Hello (ECH). Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/b090b08f52e4 branches: changeset: 2026:b090b08f52e4 user: Maxim Dounin date: Sat Sep 20 02:18:24 2025 +0300 description: Tests: added tests for Encrypted Client Hello (ECH). diffstat: ssl_encrypted_hello.t | 417 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 417 insertions(+), 0 deletions(-) diffs (422 lines): diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t new file mode 100644 --- /dev/null +++ b/ssl_encrypted_hello.t @@ -0,0 +1,417 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for http ssl module, support for Encrypted Client Hello (ECH). + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new() + ->has(qw/http http_ssl sni rewrite/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8443 ssl; + server_name public; + + ssl_certificate public.crt; + ssl_certificate_key public.key; + + ssl_encrypted_hello_key public.ech; + + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; + } + + server { + listen 127.0.0.1:8443 ssl; + server_name secret; + + ssl_certificate secret.crt; + ssl_certificate_key secret.key; + + return 200 "$ssl_server_name:$ssl_encrypted_hello\n"; + } + + server { + listen 127.0.0.1:8443 ssl; + server_name verify; + + ssl_certificate verify.crt; + ssl_certificate_key verify.key; + + ssl_verify_client optional_no_ca; + ssl_client_certificate verify.crt; + + return 200 "$ssl_server_name:$ssl_encrypted_hello:$ssl_client_verify\n"; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('public', 'secret', 'verify') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->write_file( + 'trusted.crt', + $t->read_file('public.crt') + . $t->read_file('secret.crt') + . $t->read_file('verify.crt') +); + +if ((`openssl ech -help 2>&1` || '') =~ m/-public_name/) { + + # Generate ECH file with "openssl ech" + + system('openssl ech ' + . "-out $d/public.ech " + . "-public_name public " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ECH config: $!\n"; + +} elsif ((`bssl 2>&1` || '') =~ m/generate-ech/) { + + # Generate ECH file with "bssl generate-ech" + # and additional manual formatting to produce a PEM file + + system('bssl generate-ech ' + . "-out-ech-config $d/public.echconfig.bin " + . "-out-ech-config-list $d/public.echconfiglist.bin " + . "-out-private-key $d/public.echkey.bin " + . "-public-name public " + . "-config-id 0 " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create ECH config: $!\n"; + + my $list = $t->read_file('public.echconfiglist.bin'); + my $key = $t->read_file('public.echkey.bin'); + + # BoringSSL uses raw X25519 private key. Convert it to PKCS#8 + # PrivateKeyInfo. + + $key = "\x30\x2E" # SEQUENCE, 46 bytes + . "\x02\x01\x00" # INTEGER, 1 byte, 0 + . "\x30\x05" # SEQUENCE, 5 bytes + . "\x06\x03\x2B\x65\x6E" # OBJECT, 3 bytes, X25519 + . "\x04\x22" # OCTET STRING, 34 bytes + . "\x04\x20" # OCTET STRING, 32 bytes + . $key; + + $t->write_file( + 'public.ech', + "-----BEGIN PRIVATE KEY-----\n" + . encode_base64($key) + . "-----END PRIVATE KEY-----\n" + . "-----BEGIN ECHCONFIG-----\n" + . encode_base64($list) + . "-----END ECHCONFIG-----\n" + ); + +} else { + plan(skip_all => 'no openssl ech or bssl generate-ech') +} + +$t->try_run('no ssl_encrypted_hello_key')->plan(8); + +############################################################################### + +my ($cmd, $req, $out); +my $port = port(8443); + +# ECH file looks like: +# +# -----BEGIN PRIVATE KEY----- +# MC4CAQAwBQYDK2VuBCIEIMhvGkKTR2gchVcurYDocK4v1Y5wac20UZzB3JB0QMVh +# -----END PRIVATE KEY----- +# -----BEGIN ECHCONFIG----- +# AEX+DQBBGwAgACC2q1Z7YDL1X4bahRyJeBZb3bwHPITBUxqFBS2CIfXCGQAEAAEA +# AQAScHVibGljLmV4YW1wbGUub3JnAAA= +# -----END ECHCONFIG----- +# +# To use on the client we need ECHCONFIG part, which contains ECHConfigList +# structure. + +my $config = $t->read_file('public.ech'); +$config =~ s/.*-----BEGIN ECHCONFIG-----(.*)-----END.*/$1/s; +$config =~ s/[\n\r\s]//g; + +# Requests to use + +$t->write_file('req-secret', "GET / HTTP/1.0\nHost: secret\n\n"); +$t->write_file('req-verify', "GET / HTTP/1.0\nHost: verify\n\n"); + +SKIP: { +skip 'no openssl client ech', 4 + if `openssl s_client -help 2>&1` !~ /-ech_config_list/; + +# Tests with OpenSSL s_client from ECH feature branch + +# Note that OpenSSL s_client prints confusing "ECH: BAD NAME: -102" status +# when it is not able to verify server certificate. To make sure proper +# success is visible in the output, we therefore explicitly provide trusted +# root certificates. +# +# Further, with TLSv1.2 and older protocols enabled OpenSSL s_client currently +# creates incorrect inner ClientHello, which is rejected by BoringSSL with +# the following error on the server: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines: +# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines: +# OPENSSL_internal:DECRYPTION_FAILED)... +# +# As a workaround, we explicitly request TLSv1.3 only. + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername secret " + . "-ech_config_list $config " + . "-tls1_3 " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +TODO: { +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +like($out, qr/^ECH: success.*secret:1$/ms, 'openssl client'); + +} + +# Test without ECH, to make sure the $ssl_encrypted_hello variable +# is properly set. +# +# The test explicitly requests @SECLEVEL=0 for libraries without TLSv1.2 +# support, such as OpenSSL 1.0.0. + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername secret " + . "-cipher DEFAULT:\@SECLEVEL=0 " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: NOT CONFIGURED.*secret:$/ms, 'openssl client no ech'); + +# Tests with client certificate verification, +# mostly to check if the $ssl_encrypted_hello variable is correct, notably +# with failed client certificate verification. +# +# Currently fails with OpenSSL ECH feature branch on the server, +# the error is as follows: +# +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines:: +# missing fatal)... +# +# This is expected to be fixed by +# https://github.com/openssl/openssl/pull/28555. + +TODO: { +local $TODO = 'OpenSSL broken verify' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && $t->has_feature('openssl:3.6.0'); +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername verify " + . "-ech_config_list $config " + . "-cert $d/verify.crt " + . "-key $d/verify.key " + . "-tls1_3 " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: success.*verify:1:SUCCESS/ms, 'openssl client verify'); + +$cmd = "openssl s_client " + . "-connect 127.0.0.1:$port " + . "-servername verify " + . "-ech_config_list $config " + . "-cert $d/secret.crt " + . "-key $d/secret.key " + . "-tls1_3 " + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/^ECH: success.*verify:1:FAILED/ms, + 'openssl client verify failed'); + +} +} + +SKIP: { +skip 'no bssl client ech', 4 + if (`bssl client -help 2>&1` || '') !~ /-ech-config-list/; + +# Tests with BoringSSL bssl tool + +# BoringSSL bssl tool uses a file with binary ECHConfigList +# representation. + +$t->write_file('public.bin', decode_base64($config)); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name secret " + . "-ech-config-list $d/public.bin " + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +TODO: { +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +like($out, qr/Encrypted ClientHello: yes.*secret:1$/ms, 'bssl client'); + +} + +# Test without ECH, to make sure the $ssl_encrypted_hello variable +# is properly set. +# +# The test explicitly requests TLSv1.0 for libraries without TLSv1.2 +# support, such as OpenSSL 1.0.0. + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name secret " + . "-min-version tls1 " + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: no.*secret:$/ms, 'bssl client no ech'); + +# Tests with client certificate verification, +# mostly to check if the $ssl_encrypted_hello variable is correct, notably +# with failed client certificate verification. + +TODO: { +local $TODO = 'OpenSSL broken verify' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && $t->has_feature('openssl:3.6.0'); +local $TODO = 'OpenSSL too old' + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL') + && !$t->has_feature('openssl:3.6.0'); +local $TODO = 'LibreSSL has no support yet' + if $t->has_module('LibreSSL'); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name verify " + . "-ech-config-list $d/public.bin " + . "-cert $d/verify.crt " + . "-key $d/verify.key " + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: yes.*verify:1:SUCCESS/ms, + 'bssl client verify'); + +$cmd = "bssl client " + . "-connect 127.0.0.1:$port " + . "-server-name verify " + . "-ech-config-list $d/public.bin " + . "-cert $d/secret.crt " + . "-key $d/secret.key " + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1"; + +log_out($cmd); + +$out = `$cmd`; + +log_in($out); + +like($out, qr/Encrypted ClientHello: yes.*verify:1:FAILED/ms, + 'bssl client verify failed'); + +} +} + +############################################################################### From mdounin at mdounin.ru Sun Sep 21 01:47:09 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 21 Sep 2025 04:47:09 +0300 Subject: [PATCH] Documented ssl_encrypted_hello_key and $ssl_encrypted_hello Message-ID: <2ed65baa79a5665c4269.1758419229@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1758418849 -10800 # Sun Sep 21 04:40:49 2025 +0300 # Node ID 2ed65baa79a5665c4269f7e846d9fd709d3359b8 # Parent be65dd520e819e2ea78516e64f8a47be6defae10 Documented ssl_encrypted_hello_key and $ssl_encrypted_hello. diff --git a/xml/en/docs/http/ngx_http_ssl_module.xml b/xml/en/docs/http/ngx_http_ssl_module.xml --- a/xml/en/docs/http/ngx_http_ssl_module.xml +++ b/xml/en/docs/http/ngx_http_ssl_module.xml @@ -10,7 +10,7 @@ + rev="65">
@@ -448,6 +448,87 @@ it is important to include the curves us + +file + +http +server +1.29.2 + + +Specifies a file with a secret key and a configuration +for the +Encrypted +Client Hello (ECH) TLS extension. +The $ssl_encrypted_hello variable +indicates whether the extension was used during the handshake. + + + +If several keys are specified, +all keys are used for decryption, +and only the configuration corresponding to the first key +is provided to clients for retries. +This allows configuring key rotation, for example: + +ssl_encrypted_hello_key current.ech; +ssl_encrypted_hello_key previous.ech; + + + + +The file must contain a private key and a configuration +in the +PEM +format, +and can be generated with the following command when using the +OpenSSL +ECH feature branch: + +openssl ech -public_name public.example.com -out example.ech + +Alternatively, +?bssl generate-ech? from BoringSSL can be used, +but conversion will be needed, for example: + +bssl generate-ech \ + -out-ech-config example.echconfig.bin \ + -out-ech-config-list example.echconfiglist.bin \ + -out-private-key example.echkey.bin \ + -config-id 0 \ + -public-name public.example.com +(echo "-----BEGIN PRIVATE KEY-----"; + (printf "\060\056\002\001\000\060\005\006\003\053\145\156\004\042\004\040"; + cat example.echkey.bin) | openssl base64; + echo "-----END PRIVATE KEY-----"; + echo "-----BEGIN ECHCONFIG-----"; + cat example.echconfiglist.bin | openssl base64; + echo "-----END ECHCONFIG-----") > example.ech + + + + +Decryption occurs before +the server name requested by the client is known, +and therefore +keys should be specified for the default server +if the directive is used +on the level. +See +?Virtual +server selection? section for details. + + + +The directive is supported when using the +OpenSSL +ECH feature branch or +BoringSSL. + + + + + on | off | @@ -1176,6 +1257,13 @@ TLS 1.3 early and the handshake is not complete, otherwise ?? (1.15.3). +$ssl_encrypted_hello + +returns ?1? if +the Encrypted Client Hello (ECH) +TLS extension was used during the handshake, otherwise ?? (1.29.2). + + $ssl_protocol returns the protocol of an established SSL connection; diff --git a/xml/ru/docs/http/ngx_http_ssl_module.xml b/xml/ru/docs/http/ngx_http_ssl_module.xml --- a/xml/ru/docs/http/ngx_http_ssl_module.xml +++ b/xml/ru/docs/http/ngx_http_ssl_module.xml @@ -10,7 +10,7 @@ + rev="65">
@@ -451,6 +451,86 @@ ssl_ecdh_curve prime256v1:secp384r1; + +???? + +http +server +1.29.2 + + +?????? ???? ? ????????? ?????? ? ????????????? +??? ?????????? +Encrypted +Client Hello (ECH) ????????? TLS. +?????????? $ssl_encrypted_hello +??????????, ?????????????? ?? ?????????? ??? ???????????? ??????????. + + + +???? ??????? ????????? ??????, +?? ??? ????? ????? ?????????????? ??? ???????????, +? ?????? ????????????, ??????????????? ??????? ?????, +????? ???????????? ???????? ??? ????????? ??????? ??????????. +??? ????????? ????????? ??????? ??????, ????????: + +ssl_encrypted_hello_key current.ech; +ssl_encrypted_hello_key previous.ech; + + + + +???? ?????? ????????? ????????? ???? ? ???????????? ? +??????? PEM +?, ??? ????????????? +OpenSSL +ECH feature branch, +????? ???? ?????? ????????? ????????: + +openssl ech -public_name public.example.com -out example.ech + +????? +????? ???????????? ?bssl generate-ech? ?? BoringSSL, +?? ??????????? ???????????, ????????: + +bssl generate-ech \ + -out-ech-config example.echconfig.bin \ + -out-ech-config-list example.echconfiglist.bin \ + -out-private-key example.echkey.bin \ + -config-id 0 \ + -public-name public.example.com +(echo "-----BEGIN PRIVATE KEY-----"; + (printf "\060\056\002\001\000\060\005\006\003\053\145\156\004\042\004\040"; + cat example.echkey.bin) | openssl base64; + echo "-----END PRIVATE KEY-----"; + echo "-----BEGIN ECHCONFIG-----"; + cat example.echconfiglist.bin | openssl base64; + echo "-----END ECHCONFIG-----") > example.ech + + + + +??????????? ?????????? ?? ????, ??? +??? ???????, ??????????? ????????, ?????????? ????????, +???????, ???? ????????? ???????????? +?? ?????? , +????? ?????????? ????????? ??? ??????? ?? ?????????. +????????? ??. ? ??????? +?????? +???????????? ????????. + + + +????????? ?????????????? ??? ????????????? +OpenSSL +ECH feature branch ??? +BoringSSL. + + + + + on | off | @@ -1183,6 +1263,14 @@ 0x001d:prime256v1:secp521r1:secp384r1 ? ???????? handshake ?? ?????????, ????? ?? (1.15.3). +$ssl_encrypted_hello + +?????????? ?1?, ???? +??? ???????????? ?????????? ?????????????? ?????????? +Encrypted Client Hello (ECH) +????????? TLS, ????? ?? (1.29.2). + + $ssl_protocol ?????????? ???????? ?????????????? SSL-??????????; From mdounin at mdounin.ru Mon Sep 22 21:35:56 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 00:35:56 +0300 Subject: [nginx-site] Documented ssl_encrypted_hello_key and $ssl_encrypt... Message-ID: details: http://freenginx.org/hg/nginx-site/rev/2ed65baa79a5 branches: changeset: 3118:2ed65baa79a5 user: Maxim Dounin date: Sun Sep 21 04:40:49 2025 +0300 description: Documented ssl_encrypted_hello_key and $ssl_encrypted_hello. diffstat: xml/en/docs/http/ngx_http_ssl_module.xml | 90 +++++++++++++++++++++++++++++++- xml/ru/docs/http/ngx_http_ssl_module.xml | 90 +++++++++++++++++++++++++++++++- 2 files changed, 178 insertions(+), 2 deletions(-) diffs (228 lines): diff --git a/xml/en/docs/http/ngx_http_ssl_module.xml b/xml/en/docs/http/ngx_http_ssl_module.xml --- a/xml/en/docs/http/ngx_http_ssl_module.xml +++ b/xml/en/docs/http/ngx_http_ssl_module.xml @@ -10,7 +10,7 @@ + rev="65">
@@ -448,6 +448,87 @@ it is important to include the curves us + +file + +http +server +1.29.2 + + +Specifies a file with a secret key and a configuration +for the +Encrypted +Client Hello (ECH) TLS extension. +The $ssl_encrypted_hello variable +indicates whether the extension was used during the handshake. + + + +If several keys are specified, +all keys are used for decryption, +and only the configuration corresponding to the first key +is provided to clients for retries. +This allows configuring key rotation, for example: + +ssl_encrypted_hello_key current.ech; +ssl_encrypted_hello_key previous.ech; + + + + +The file must contain a private key and a configuration +in the +PEM +format, +and can be generated with the following command when using the +OpenSSL +ECH feature branch: + +openssl ech -public_name public.example.com -out example.ech + +Alternatively, +?bssl generate-ech? from BoringSSL can be used, +but conversion will be needed, for example: + +bssl generate-ech \ + -out-ech-config example.echconfig.bin \ + -out-ech-config-list example.echconfiglist.bin \ + -out-private-key example.echkey.bin \ + -config-id 0 \ + -public-name public.example.com +(echo "-----BEGIN PRIVATE KEY-----"; + (printf "\060\056\002\001\000\060\005\006\003\053\145\156\004\042\004\040"; + cat example.echkey.bin) | openssl base64; + echo "-----END PRIVATE KEY-----"; + echo "-----BEGIN ECHCONFIG-----"; + cat example.echconfiglist.bin | openssl base64; + echo "-----END ECHCONFIG-----") > example.ech + + + + +Decryption occurs before +the server name requested by the client is known, +and therefore +keys should be specified for the default server +if the directive is used +on the level. +See +?Virtual +server selection? section for details. + + + +The directive is supported when using the +OpenSSL +ECH feature branch or +BoringSSL. + + + + + on | off | @@ -1176,6 +1257,13 @@ TLS 1.3 early and the handshake is not complete, otherwise ?? (1.15.3). +$ssl_encrypted_hello + +returns ?1? if +the Encrypted Client Hello (ECH) +TLS extension was used during the handshake, otherwise ?? (1.29.2). + + $ssl_protocol returns the protocol of an established SSL connection; diff --git a/xml/ru/docs/http/ngx_http_ssl_module.xml b/xml/ru/docs/http/ngx_http_ssl_module.xml --- a/xml/ru/docs/http/ngx_http_ssl_module.xml +++ b/xml/ru/docs/http/ngx_http_ssl_module.xml @@ -10,7 +10,7 @@ + rev="65">
@@ -451,6 +451,86 @@ ssl_ecdh_curve prime256v1:secp384r1; + +???? + +http +server +1.29.2 + + +?????? ???? ? ????????? ?????? ? ????????????? +??? ?????????? +Encrypted +Client Hello (ECH) ????????? TLS. +?????????? $ssl_encrypted_hello +??????????, ?????????????? ?? ?????????? ??? ???????????? ??????????. + + + +???? ??????? ????????? ??????, +?? ??? ????? ????? ?????????????? ??? ???????????, +? ?????? ????????????, ??????????????? ??????? ?????, +????? ???????????? ???????? ??? ????????? ??????? ??????????. +??? ????????? ????????? ??????? ??????, ????????: + +ssl_encrypted_hello_key current.ech; +ssl_encrypted_hello_key previous.ech; + + + + +???? ?????? ????????? ????????? ???? ? ???????????? ? +??????? PEM +?, ??? ????????????? +OpenSSL +ECH feature branch, +????? ???? ?????? ????????? ????????: + +openssl ech -public_name public.example.com -out example.ech + +????? +????? ???????????? ?bssl generate-ech? ?? BoringSSL, +?? ??????????? ???????????, ????????: + +bssl generate-ech \ + -out-ech-config example.echconfig.bin \ + -out-ech-config-list example.echconfiglist.bin \ + -out-private-key example.echkey.bin \ + -config-id 0 \ + -public-name public.example.com +(echo "-----BEGIN PRIVATE KEY-----"; + (printf "\060\056\002\001\000\060\005\006\003\053\145\156\004\042\004\040"; + cat example.echkey.bin) | openssl base64; + echo "-----END PRIVATE KEY-----"; + echo "-----BEGIN ECHCONFIG-----"; + cat example.echconfiglist.bin | openssl base64; + echo "-----END ECHCONFIG-----") > example.ech + + + + +??????????? ?????????? ?? ????, ??? +??? ???????, ??????????? ????????, ?????????? ????????, +???????, ???? ????????? ???????????? +?? ?????? , +????? ?????????? ????????? ??? ??????? ?? ?????????. +????????? ??. ? ??????? +?????? +???????????? ????????. + + + +????????? ?????????????? ??? ????????????? +OpenSSL +ECH feature branch ??? +BoringSSL. + + + + + on | off | @@ -1183,6 +1263,14 @@ 0x001d:prime256v1:secp521r1:secp384r1 ? ???????? handshake ?? ?????????, ????? ?? (1.15.3). +$ssl_encrypted_hello + +?????????? ?1?, ???? +??? ???????????? ?????????? ?????????????? ?????????? +Encrypted Client Hello (ECH) +????????? TLS, ????? ?? (1.29.2). + + $ssl_protocol ?????????? ???????? ?????????????? SSL-??????????; From mdounin at mdounin.ru Mon Sep 22 22:00:02 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 23 Sep 2025 01:00:02 +0300 Subject: freenginx-1.29.2 changes draft Message-ID: Hello! Changes with freenginx 1.29.2 23 Sep 2025 *) Feature: support for the Encrypted Client Hello (ECH) extension of the TLS 1.3 protocol. ????????? ? freenginx 1.29.2 23.09.2025 *) ??????????: ????????? ?????????? Encrypted Client Hello (ECH) ????????? TLS 1.3. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue Sep 23 16:10:12 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 19:10:12 +0300 Subject: [nginx] Updated PCRE2 used for win32 builds. Message-ID: details: http://freenginx.org/hg/nginx/rev/2c6e0b66de6a branches: changeset: 9424:2c6e0b66de6a user: Maxim Dounin date: Tue Sep 23 04:58:51 2025 +0300 description: Updated PCRE2 used for win32 builds. diffstat: misc/GNUmakefile | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/misc/GNUmakefile b/misc/GNUmakefile --- a/misc/GNUmakefile +++ b/misc/GNUmakefile @@ -8,7 +8,7 @@ CC = cl OBJS = objs.msvc8 OPENSSL = openssl-3.0.17 ZLIB = zlib-1.3.1 -PCRE = pcre2-10.45 +PCRE = pcre2-10.46 release: export From mdounin at mdounin.ru Tue Sep 23 16:10:12 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 19:10:12 +0300 Subject: [nginx] freenginx-1.29.2-RELEASE Message-ID: details: http://freenginx.org/hg/nginx/rev/56d817adaa1d branches: changeset: 9425:56d817adaa1d user: Maxim Dounin date: Tue Sep 23 19:06:46 2025 +0300 description: freenginx-1.29.2-RELEASE diffstat: docs/xml/nginx/changes.xml | 14 ++++++++++++++ 1 files changed, 14 insertions(+), 0 deletions(-) diffs (24 lines): diff --git a/docs/xml/nginx/changes.xml b/docs/xml/nginx/changes.xml --- a/docs/xml/nginx/changes.xml +++ b/docs/xml/nginx/changes.xml @@ -7,6 +7,20 @@
+ + + + +????????? ?????????? Encrypted Client Hello (ECH) ????????? TLS 1.3. + + +support for the Encrypted Client Hello (ECH) extension of the TLS 1.3 protocol. + + + + + + From mdounin at mdounin.ru Tue Sep 23 16:10:12 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 19:10:12 +0300 Subject: [nginx] release-1.29.2 tag Message-ID: details: http://freenginx.org/hg/nginx/rev/618107533561 branches: changeset: 9426:618107533561 user: Maxim Dounin date: Tue Sep 23 19:06:47 2025 +0300 description: release-1.29.2 tag diffstat: .hgtags | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (8 lines): diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -487,3 +487,4 @@ e5a159b0c3821bf4e93b8e9ee34604238c93fd2a 6731069e4b635d9dca49d6de04f0241cf3d856dd release-1.27.6 441d59c1052d602143764a5765d65c51388c1d7b release-1.29.0 8ccd477736c998c0ea64c477b75c647cb28b356c release-1.29.1 +56d817adaa1dd4f522114e921b1ff599c2ab4db2 release-1.29.2 From mdounin at mdounin.ru Tue Sep 23 16:13:45 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 19:13:45 +0300 Subject: [nginx-site] freenginx-1.29.2 Message-ID: details: http://freenginx.org/hg/nginx-site/rev/8a5c9c1d351c branches: changeset: 3119:8a5c9c1d351c user: Maxim Dounin date: Tue Sep 23 19:10:59 2025 +0300 description: freenginx-1.29.2 diffstat: text/en/CHANGES | 6 ++++++ text/ru/CHANGES.ru | 6 ++++++ xml/index.xml | 12 ++++++++++++ xml/versions.xml | 1 + 4 files changed, 25 insertions(+), 0 deletions(-) diffs (61 lines): diff --git a/text/en/CHANGES b/text/en/CHANGES --- a/text/en/CHANGES +++ b/text/en/CHANGES @@ -1,4 +1,10 @@ +Changes with freenginx 1.29.2 23 Sep 2025 + + *) Feature: support for the Encrypted Client Hello (ECH) extension of + the TLS 1.3 protocol. + + Changes with freenginx 1.29.1 26 Aug 2025 *) Change: now responses from proxied servers over HTTP/0.9 are rejected diff --git a/text/ru/CHANGES.ru b/text/ru/CHANGES.ru --- a/text/ru/CHANGES.ru +++ b/text/ru/CHANGES.ru @@ -1,4 +1,10 @@ +????????? ? freenginx 1.29.2 23.09.2025 + + *) ??????????: ????????? ?????????? Encrypted Client Hello (ECH) + ????????? TLS 1.3. + + ????????? ? freenginx 1.29.1 26.08.2025 *) ?????????: ?????? ?????? ???????????? ???????? ?? HTTP/0.9 ?? diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -8,6 +8,18 @@ + + +freenginx-1.29.1 +mainline version has been released, +featuring +support for the +Encrypted Client Hello (ECH) +extension of the TLS 1.3 protocol. + + + freenginx-1.29.1 diff --git a/xml/versions.xml b/xml/versions.xml --- a/xml/versions.xml +++ b/xml/versions.xml @@ -9,6 +9,7 @@ + From mdounin at mdounin.ru Tue Sep 23 16:17:19 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 23 Sep 2025 19:17:19 +0300 Subject: [nginx-site] Fixed wrong version number in news item. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/6f11c0a05256 branches: changeset: 3120:6f11c0a05256 user: Maxim Dounin date: Tue Sep 23 19:16:42 2025 +0300 description: Fixed wrong version number in news item. diffstat: xml/index.xml | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -10,7 +10,7 @@ -freenginx-1.29.1 +freenginx-1.29.2 mainline version has been released, featuring support for the