From 85d74376925fdd5e61b6eabd708718fc3cdbf85f Mon Sep 17 00:00:00 2001 From: sftcd Date: Tue, 12 Aug 2025 22:08:10 +0100 Subject: [PATCH 1/6] OpenSSL ECH integration --- ECH-build.md | 325 +++++++++++++++++++++++++ src/event/ngx_event_openssl.c | 203 +++++++++++++++ src/event/ngx_event_openssl.h | 19 ++ src/http/modules/ngx_http_log_module.c | 65 +++++ src/http/modules/ngx_http_ssl_module.c | 29 +++ src/http/modules/ngx_http_ssl_module.h | 3 + 6 files changed, 644 insertions(+) create mode 100644 ECH-build.md diff --git a/ECH-build.md b/ECH-build.md new file mode 100644 index 000000000..c6eee1f4f --- /dev/null +++ b/ECH-build.md @@ -0,0 +1,325 @@ + +# NGINX OpenSSL Encrypted Client Hello (ECH) integration. + +> [!NOTE] +> This documentation probably doesn't belong here, nor as a single file, but +> may be useful to have in one place as we process the PR. TODO: find out where +> to put the various bits and pieces once those are stable. + +ECH is specified in +[draft-ietf-tls-esni](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/). +This documentation assumes a basic familiarity with the ECH specification. + +This build only supports ECH "shared-mode" where the NGINX instance does the +ECH decryption and also hosts both the ECH `public-name` and `backend` web +sites. ECH "split-mode" where the NGINX instance only does ECH decryption but +passes the TLS session on to a different backend service requires changes to +OpenSSL that have yet to be merged to the ECH feature branch. There is a +separate proof-of-concept implementation for that, but that is not documented +here. (For more on ECH "split-mode" see the +[defo-project-PoC](https://github.com/defo-project/ech-dev-utils/blob/main/howtos/nginx.md).) + +## Build + +> [!NOTE] +> ECH is not yet a part of an OpenSSL release, our current goal is that ECH be +> part of an OpenSSL 4.0 release in spring 2026. + +There is client and server ECH code in the OpenSSL ECH feature branch at +[https://github.com/openssl/openssl/tree/feature/ech](https://github.com/openssl/openssl/tree/feature/ech). +At present, ECH-enabling NGINX therefore requires building from source, using +the OpenSSL ECH feature branch. + +To get the ECH feature branch: + +```bash +$ cd /home/user/code +$ git clone https://github.com/openssl/openssl/ openssl-for-nginx +$ cd openssl-for-nginx +$ git checkout feature/ech +``` + +Then an option to build NGINX is: + +```bash +$ cd /home/user/code +$ git clone https://github.com/sftcd/nginx.git +$ cd nginx +$ ./auto/configure --with-debug --prefix=nginx --with-http_ssl_module --with-openssl=/home/user/code/openssl-for-nginx --with-openssl-opt="--debug" --with-http_v2_module +$ make +...stuff... +``` + +This results in an NGINX binary in `objs/nginx` with a statically linked +OpenSSL, so as not to disturb system libraries. + +## ECH Key Generation and Publication + +In the remaining, we describe a configuration that uses `example.com` as the +ECH `public-name` and where `foo.example.com` is a web-site for which we want +ECH to be used, with both hosted on the same NGINX instance. + +Using ECH requries that NGINX load an ECH key pair with a private value for ECH +decryption. Browsers will require that the public component of that key pair be +published in the DNS. With OpenSSL we generate and store that key pair in a PEM +formatted file as shown below. + +To generate ECH PEM files, use the openssl binary produced by the build above +(which is `/home/user/code/openssl-for-nginx/.openssl/bin/openssl`) to generate +an ECH key pair and store the result in a PEM file. You should also supply the +`public-name` required by the ECH protocol. + +Key generation operations should be carried out under whatever local account is +used for NGINX configuration. + +```bash +~# OSSL=/home/user/code/openssl-for-nginx/.openssl/bin/openssl +~# mkdir -p /etc/nginx/echkeydir +~# chmod 700 /etc/nginx/echkeydir +~# cd /etc/nginx/echkeydir +~# $OSSL ech -public-name example.com -o example.com.pem.ech +~# cat example.com.pem.ech +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIJi22Im2rJ/lJqzNFZdGfsVfmknXAc8xz3fYPhD0Na5I +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +AD7+DQA6QwAgACA8mxkEsSTp2xXC/RUFCC6CZMMgdM4x1iTWKu3EONjbMAAEAAEA +AQALZXhhbXBsZS5vcmcAAA== +-----END ECHCONFIG----- +``` + +> [!NOTE] +> The January 2025 lighttpd web server release included ECH and adopted a +> naming convention for ECH PEM files that their names ought end in `.ech`. +> This PR follows that covention. + +The ECHConfig value then needs to be published in an HTTPS resource record in +the DNS, so as to be accessible as shown below: + +```bash +$ dig +short HTTPS foo.example.com +1 . ech=AD7+DQA6QwAgACA8mxkEsSTp2xXC/RUFCC6CZMMgdM4x1iTWKu3EONjbMAAEAAEAAQALZXhhbXBsZS5vcmcAAA== +$ +``` + +Various other fields may be included in an HTTPS resource record. For many +NGINX instances, existing methods for publishing DNS records may be used to +achieve the above. In some cases, one might use [A well-known URI for +publishing service +parameters](https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech) +designed to assist web servers in handling e.g. frequent ECH key rotation. + +## Configuration + +To enable ECH for an NGINX instance, configure a directory name via the +`ssl_echkeydir` directive where that directory contains a set of ECH PEM key +files. The `ssl_echkeydir` directive should be in the "http" section of an +NGINX configuration as shown in the example below. All ECH PEM files in that +directory that are successfully decoded will be loaded. + +``` +http { + log_format withech '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" "$ech_status"'; + access_log /var/log/nginx/access.log withech; + ssl_echkeydir /etc/nginx/echkeydir; + server { + listen 443 default_server ssl; + http2 on; + ssl_certificate /etc/nginx/example.com.crt; + ssl_certificate_key /etc/nginx/example.com.priv; + ssl_protocols TLSv1.3; + server_name example.com; + location / { + root /var/www/dir-example.com; + index index.html index.htm; + } + } + server { + listen 443 ssl; + http2 on; + ssl_certificate /etc/nginx/example.com.crt; + ssl_certificate_key /etc/nginx/example.com.priv; + ssl_protocols TLSv1.3; + server_name foo.example.com; + location / { + root /var/www/dir-foo.example.com; + index index.html index.htm; + } + } +``` + +## Logs + +You can log ECH status information in the normal `access.log` by adding +`$ech_status` to the `log_format`, e.g. the stanza below adds ECH status to the +normal `combined` log format: + +``` + log_format withech '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" "$ech_status"'; + access_log /var/log/nginx/access.log withech; +``` + +That results in log lines like the following: + +``` +127.0.0.1 - - [26/Feb/2025:13:35:52 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_GREASE/foo.example.com/" +127.0.0.1 - - [26/Feb/2025:13:39:39 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_NOT_TRIED/foo.example.com/" +127.0.0.1 - - [26/Feb/2025:14:08:21 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_SUCCESS/example.com/foo.example.com" +127.0.0.1 - - [26/Feb/2025:14:09:58 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_NOT_TRIED/foo.example.com/" +127.0.0.1 - - [26/Feb/2025:14:11:47 +0000] "GET / HTTP/1.1" 400 255 "-" "curl/8.12.0-DEV" "ECH: no TLS connection" +``` + +When ECH has succeeded, then the outer SNI and inner SNI are included in that +order. If a client GREASEd or didn't try ECH at all, and no outer SNI was +provided, the HTTP host header will be shown instead. Connections that did not +use TLS show that. The TLS version is not specifically shown, so TLSv1.2 +connections will show up as `SSL_ECH_STATUS_NOT_TRIED`. + +At start-up, and on configuration re-load, NGINX will log (to `error.log` at +the "notice" log level) the names of ECH PEM files successfully loaded and the +total number of ECH keys loaded, for each `server` stanza in the configuration. +Errors in loading keys are also logged and may result in the server not +starting. + +## CGI variables + +We set the following variables for, e.g. PHP code: + +- ``SSL_ECH_STATUS`` - ``success`` means that, others also mean what they say +- ``SSL_ECH_INNER_SNI`` - has value that was in inner ClientHello SNI (or + ``NONE``) +- ``SSL_ECH_OUTER_SNI`` - has value that was in outer ClientHello SNI (or + ``NONE``) + +To see those using fastcgi you need to include the following in the relevant +NGINX config: + +``` +fastcgi_param SSL_ECH_STATUS $ssl_ech_status; +fastcgi_param SSL_ECH_INNER_SNI $ssl_ech_inner_sni; +fastcgi_param SSL_ECH_OUTER_SNI $ssl_ech_outer_sni; +``` + +## Code changes + +- New code is protected using `#ifndef OPENSSL_NO_ECH` as is done in the + OpenSSL ECH feature branch. That is set in `src/event/ngx_event_openssl.h` if + the new ECH symbol `SSL_OP_ECH_GREASE` is not defined in `ssl.h`. In other + words, if NGINX is built using an OpenSSL version that has ECH support, then + that will be used. If the OpenSSL version doesn't have ECH then the + ECH-specific code in NGINX is compiled out. + +- `src/http/modules/ngx_http_ssl_module.h` and + `src/http/modules/ngx_http_ssl_module.c` define the new `ssl_echkeydir` + directive and the variables that become visible to e.g. PHP code. + +- `load_echkeys()` in `src/event/ngx_event_openssl.c` loads ECH PEM files as + directed by the `ssl_echkeydir` directive, and enables shared-mode ECH + decryption if some ECH keys are loaded. If `ssl_echkeydir` is set, but no keys + are loaded, that results in an error and NGINX exits. + +- `ngx_ssl_get_ech_status()`, `ngx_ssl_get_ech_inner_sni()` and + `ngx_ssl_get_ech_outer_sni()` also in `src/event/ngx_event_openssl.c` provide + for setting the CGI variables mentioned above. + +- `src/http/modules/ngx_http_log_module.c` contains code to handle the new + `$ech_status` log format, mainly in the `ngx_http_log_ech_status()` function. + +> [!NOTE] +> `load_echkeys()` will include the public component all loaded keys in the ECH +> `retry-configs` in the fallback scenario. If desired, we could add a naming +> convention or additional configuration setting to distinguish which to +> include in `retry-configs` or not. For now, we assume that'd better be done +> in a subsequent PR, if experience shows the feature is really useful/needed. +> (We can envisage some odd deployments where that might be the case, but not +> clear those'd really happen - it'd seem to need loads of key pairs or else +> some that are never published in the DNS that we don't want to expose to +> random clients - neither seems compelling.) + +## Reloading ECH keys + +ECH uses a form of ephemeral-static (Elliptic curve) Diffie-Hellman key +exchange, so in order to get better forward secrecy, there is a need to perhaps +frequently rotate ECH keys. For example, some widely-used ECH-enabled web +services rotate ECH keys hourly. That may be done e.g. via a cronjob and using +[A well-known URI for publishing service +parameters](https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech). In +such a setup, the set of ECH PEM files in the `ssl_echkeydir` directory will +change hourly, with the directory likely to contain perhaps three ECH PEM files +(curent, hour-before and two-hours before). This creates a need to reload ECH +PEM files regularly. + +Sending a SIGHUP signal to the running process causes it to reload it's +configuration, so if `$PIDFILE` is a file with the NGINX server process-id: + +```bash +$ kill -SIGHUP `cat $PIDFILE` +``` + +When ECH PEM files are loaded or re-loaded that's logged to the error log, +e.g.: + +``` +2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, total keys loaded: 2 +2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/echconfig.pem.ech +2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/d13.pem.ech +``` + +> [!NOTE] +> The ECH integration released by the lighttpd web server in January 2025 +> allows configuration of a timer used to cause ECH PEM files to be reloaded if +> those have changed. This PR does not include that functionality but it could +> be added if desired, e.g. if regularly reloading the entire NGINX +> configuration is considered undesirable. See the [lighttpd +> code](https://github.com/lighttpd/lighttpd1.4/blob/master/src/mod_openssl.c#L799) +> for details. + +## Debugging + +To run NGINX in ``gdb`` you probably want to uncomment the ``daemon off;`` and +``master_process off;`` lines in your config file. You probably also want to +build with `CFLAGS="-g -O0"` to turn off optimization, and then, e.g. if you +wanted to debug into the ``load_echkeys()`` function: + +```bash + $ gdb ~/code/nginx/objs/nginx + GNU gdb (Ubuntu 13.1-2ubuntu2) 13.1 + Copyright (C) 2023 Free Software Foundation, Inc. + License GPLv3+: GNU GPL version 3 or later + This is free software: you are free to change and redistribute it. + There is NO WARRANTY, to the extent permitted by law. + Type "show copying" and "show warranty" for details. + This GDB was configured as "x86_64-linux-gnu". + Type "show configuration" for configuration details. + For bug reporting instructions, please see: + . + Find the GDB manual and other documentation resources online at: + . + + For help, type "help". + Type "apropos word" to search for commands related to "word"... + Reading symbols from /home/user/code/nginx/objs/nginx... + (gdb) b load_echkeys + Breakpoint 1 at 0x1402e9: file src/event/ngx_event_openssl.c, line 1469. + (gdb) r -c nginxmin.conf + Starting program: /home/user/code/nginx/objs/nginx -c nginxmin.conf + [Thread debugging using libthread_db enabled] + Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". + + Breakpoint 1, load_echkeys (ssl=ssl@entry=0x555555db64d8, dirname=dirname@entry=0x555555db6568) + at src/event/ngx_event_openssl.c:1469 + 1469 { + (gdb) c + Continuing. + + Breakpoint 1, load_echkeys (ssl=ssl@entry=0x555555dbad68, dirname=dirname@entry=0x555555dbadf8) + at src/event/ngx_event_openssl.c:1469 + 1469 { + (gdb) c + Continuing. + [Detaching after fork from child process 522259] +``` diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index b1ae2955d..7aaf1f605 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -1572,6 +1572,125 @@ ngx_ssl_passwords_cleanup(void *data) } +#ifndef OPENSSL_NO_ECH +/* load key files called .ech we find in the ssl_echkeydir directory */ +static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) +{ + /* 1024 private key files (maxkeyfiles) is plenty */ + int somekeyworked = 0, numkeys = 0, maxkeyfiles=1024; + char *den = NULL, *last4 = NULL; + size_t elen = dirname->len, nlen = 0; + struct stat thestat; + ngx_dir_t thedir; + ngx_int_t nrv = ngx_open_dir(dirname, &thedir); + OSSL_ECHSTORE * const es = OSSL_ECHSTORE_new(NULL, NULL); + char privname[PATH_MAX]; + + if (es == NULL) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys, error allocating store" , __LINE__); + return NGX_ERROR; + } + if (nrv != NGX_OK) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys, error opening %s at %d", dirname->data, __LINE__); + return NGX_ERROR; + } + for (;;) { + nrv=ngx_read_dir(&thedir); + if (nrv != NGX_OK) { + break; + } + den = (char *)ngx_de_name(&thedir); + nlen = strlen(den); + if (nlen > 4) { + last4 = den + nlen - 4; + if (strncmp(last4, ".ech", 4)) { + continue; + } + if ((elen + 1 + nlen + 1) >= PATH_MAX) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys, error, name too long: %s with %s", + dirname->data, den); + continue; + } + snprintf(privname, PATH_MAX,"%s/%s", dirname->data, den); + if (!--maxkeyfiles) { + /* so we don't loop forever, ever */ + ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, + "load_echkeys, too many private key files to check!"); + ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, + "load_echkeys, maxkeyfiles is hardcoded to 1024"); + return NGX_ERROR; + } + if (stat(privname, &thestat) == 0) { + BIO *in = BIO_new_file(privname, "r"); + const int is_retry_config = OSSL_ECH_FOR_RETRY; + + if (in != NULL + && 1 == OSSL_ECHSTORE_read_pem(es, in, is_retry_config)) { + ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, + "load_echkeys, worked for: %s", privname); + somekeyworked = 1; + } + else { + ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, + "load_echkeys, failed for: %s",privname); + } + BIO_free_all(in); + } + } + } + ngx_close_dir(&thedir); + + if (somekeyworked == 0) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys failed for all keys but ECH configured"); + return NGX_ERROR; + } + if (OSSL_ECHSTORE_num_keys(es, &numkeys) != 1) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys OSSL_ECHSTORE_num_keys failed"); + return NGX_ERROR; + } + ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, + "load_echkeys, total keys loaded: %d", numkeys); + if (1 != SSL_CTX_set1_echstore(ssl->ctx, es)) { + OSSL_ECHSTORE_free(es); + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "load_echkeys: SSL_CTX_set1_echstore failed"); + return NGX_ERROR; + } + OSSL_ECHSTORE_free(es); + + return NGX_OK; +} + +ngx_int_t +ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir) +{ + int rv = 0; + + if (!dir) { + return NGX_OK; + } + if (dir->len == 0) { + return NGX_OK; + } + if (cf != NULL && ngx_conf_full_name(cf->cycle, dir, 1) != NGX_OK) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); + return NGX_ERROR; + } + rv = load_echkeys(ssl, dir); + if (rv != NGX_OK) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); + return rv; + } + return NGX_OK; +} +#endif + + ngx_int_t ngx_ssl_dhparam(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *file) { @@ -5336,6 +5455,90 @@ ngx_ssl_get_cipher_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) } +#ifndef OPENSSL_NO_ECH +ngx_int_t +ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) +{ + int echrv = SSL_ECH_STATUS_NOT_TRIED; + char *inner_sni = NULL, *outer_sni = NULL; + char buf[PATH_MAX]; + + echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); + switch (echrv) { + case SSL_ECH_STATUS_NOT_TRIED: + snprintf(buf,PATH_MAX, "not attempted"); + break; + case SSL_ECH_STATUS_FAILED: + snprintf(buf, PATH_MAX, "tried but failed"); + break; + case SSL_ECH_STATUS_BAD_NAME: + snprintf(buf, PATH_MAX, "worked but bad name"); + break; + case SSL_ECH_STATUS_SUCCESS: + snprintf(buf, PATH_MAX, "success"); + break; + case SSL_ECH_STATUS_GREASE: + snprintf(buf, PATH_MAX, "GREASEd ECH"); + break; + case SSL_ECH_STATUS_BACKEND: + snprintf(buf, PATH_MAX, "Backend/inner ECH"); + break; + default: + snprintf(buf, PATH_MAX, "error getting ECH status"); + break; + } + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + s->len = ngx_strlen(buf); + s->data = ngx_pnalloc(pool, s->len); + ngx_memcpy(s->data, buf, s->len); + return NGX_OK; +} + +ngx_int_t +ngx_ssl_get_ech_inner_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) +{ + int echrv = SSL_ECH_STATUS_NOT_TRIED; + char *inner_sni, *outer_sni; + + echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); + if (echrv == SSL_ECH_STATUS_SUCCESS && inner_sni) { + s->len = strlen(inner_sni); + s->data = ngx_pnalloc(pool, s->len); + ngx_memcpy(s->data, inner_sni, s->len); + } else { + s->len = ngx_strlen("NONE"); + s->data = ngx_pnalloc(pool, s->len); + ngx_memcpy(s->data, "NONE", s->len); + } + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + return NGX_OK; +} + +ngx_int_t +ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) +{ + int echrv = SSL_ECH_STATUS_NOT_TRIED; + char *inner_sni, *outer_sni; + + echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); + if (echrv == SSL_ECH_STATUS_SUCCESS && outer_sni) { + s->len = strlen(outer_sni); + s->data = ngx_pnalloc(pool, s->len); + ngx_memcpy(s->data, outer_sni, s->len); + } else { + s->len = ngx_strlen("NONE"); + s->data = ngx_pnalloc(pool, s->len); + ngx_memcpy(s->data, "NONE", s->len); + } + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + return NGX_OK; +} +#endif + + ngx_int_t ngx_ssl_get_ciphers(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index 79f96c160..6b3f8e816 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -34,6 +34,14 @@ #include #include +/* check defines from for ECH support */ +#if !defined(SSL_OP_ECH_GREASE) +#define OPENSSL_NO_ECH +#endif +#ifndef OPENSSL_NO_ECH +#include +#endif + #define NGX_SSL_NAME "OpenSSL" @@ -297,6 +305,9 @@ enum ssl_select_cert_result_t ngx_ssl_select_certificate( ngx_int_t ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags); +#ifndef OPENSSL_NO_ECH +ngx_int_t ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir); +#endif void ngx_ssl_remove_cached_session(SSL_CTX *ssl, ngx_ssl_session_t *sess); ngx_int_t ngx_ssl_set_session(ngx_connection_t *c, ngx_ssl_session_t *session); @@ -326,6 +337,14 @@ ngx_int_t ngx_ssl_get_ciphers(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_curve(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); +#ifndef OPENSSL_NO_ECH +ngx_int_t ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, + ngx_str_t *s); +ngx_int_t ngx_ssl_get_ech_inner_sni(ngx_connection_t *c, ngx_pool_t *pool, + ngx_str_t *s); +ngx_int_t ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, + ngx_str_t *s); +#endif ngx_int_t ngx_ssl_get_curves(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_session_id(ngx_connection_t *c, ngx_pool_t *pool, diff --git a/src/http/modules/ngx_http_log_module.c b/src/http/modules/ngx_http_log_module.c index f7c4bd2f5..01b96f0c4 100644 --- a/src/http/modules/ngx_http_log_module.c +++ b/src/http/modules/ngx_http_log_module.c @@ -129,6 +129,10 @@ static u_char *ngx_http_log_body_bytes_sent(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op); static u_char *ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op); +#ifndef OPENSSL_NO_ECH +static u_char *ngx_http_log_ech_status(ngx_http_request_t *r, u_char *buf, + ngx_http_log_op_t *op); +#endif static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, ngx_str_t *value, ngx_uint_t escape); @@ -230,6 +234,10 @@ static ngx_str_t ngx_http_combined_fmt = "\"$http_referer\" \"$http_user_agent\""); +#ifndef OPENSSL_NO_ECH +#define NGX_ECH_STATUS_LEN 140 +#endif + static ngx_http_log_var_t ngx_http_log_vars[] = { { ngx_string("pipe"), 1, ngx_http_log_pipe }, { ngx_string("time_local"), sizeof("28/Sep/1970:12:00:00 +0600") - 1, @@ -245,6 +253,10 @@ static ngx_http_log_var_t ngx_http_log_vars[] = { ngx_http_log_body_bytes_sent }, { ngx_string("request_length"), NGX_SIZE_T_LEN, ngx_http_log_request_length }, +#ifndef OPENSSL_NO_ECH + { ngx_string("ech_status"), NGX_ECH_STATUS_LEN, + ngx_http_log_ech_status }, +#endif { ngx_null_string, 0, NULL } }; @@ -911,6 +923,59 @@ ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, return ngx_sprintf(buf, "%O", r->request_length); } +#ifndef OPENSSL_NO_ECH +static u_char * +ngx_http_log_ech_status(ngx_http_request_t *r, u_char *buf, + ngx_http_log_op_t *op) +{ + int echstat = SSL_ECH_STATUS_NOT_TRIED; + SSL *ssl = NULL; + char *sni_ech = NULL, *sni_clr = NULL, *hostheader = NULL; + u_char *sprv = NULL; + const char *str; + + /* + * this is a bit oddly structured but is based on what was done for + * lighttpd (by it's upstream maintainer) and what we did for haproxy + * and re-use makes us all happy + */ + if (!r || !r->connection || !r->connection->ssl + || !r->connection->ssl->connection) + return ngx_sprintf(buf, "ECH: no TLS connection"); + ssl = r->connection->ssl->connection; + if (r->headers_in.server.len > 0) + hostheader = (char *)r->headers_in.server.data; +#define s(x) #x + switch ((echstat = SSL_ech_get1_status(ssl, &sni_ech, &sni_clr))) { + case SSL_ECH_STATUS_SUCCESS: str = s(SSL_ECH_STATUS_SUCCESS); break; + case SSL_ECH_STATUS_NOT_TRIED: str = s(SSL_ECH_STATUS_NOT_TRIED); break; + case SSL_ECH_STATUS_FAILED: str = s(SSL_ECH_STATUS_FAILED); break; + case SSL_ECH_STATUS_BAD_NAME: str = s(SSL_ECH_STATUS_BAD_NAME); break; + case SSL_ECH_STATUS_BAD_CALL: str = s(SSL_ECH_STATUS_BAD_CALL); break; + case SSL_ECH_STATUS_GREASE: str = s(SSL_ECH_STATUS_GREASE); break; + case SSL_ECH_STATUS_BACKEND: str = s(SSL_ECH_STATUS_BACKEND); break; + default: str = "ECH status unknown"; break; + } +#undef s + /* + * We output ECH status, then either the outer SNI or the host header (if + * outer SNI is NULL) and the inner SNI if non-NULL. + */ + if (echstat != SSL_ECH_STATUS_SUCCESS) { + OPENSSL_free(sni_clr); + sni_clr = (char *)SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + } + if (sni_clr != NULL) + hostheader = sni_clr; + sprv = ngx_sprintf(buf, "ECH: %s/%s/%s", str, + (hostheader == NULL ? "" : hostheader), + (sni_ech == NULL ? "" : sni_ech)); + OPENSSL_free(sni_ech); + if (echstat == SSL_ECH_STATUS_SUCCESS) + OPENSSL_free(sni_clr); + return sprv; +} +#endif static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c index 7a6f49c3f..affac94e6 100644 --- a/src/http/modules/ngx_http_ssl_module.c +++ b/src/http/modules/ngx_http_ssl_module.c @@ -215,6 +215,15 @@ static ngx_command_t ngx_http_ssl_commands[] = { offsetof(ngx_http_ssl_srv_conf_t, session_tickets), NULL }, +#ifndef OPENSSL_NO_ECH + { ngx_string("ssl_echkeydir"), + NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_ssl_srv_conf_t, echkeydir), + NULL }, +#endif + { ngx_string("ssl_session_ticket_key"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_array_slot, @@ -354,6 +363,14 @@ static ngx_http_variable_t ngx_http_ssl_vars[] = { { ngx_string("ssl_curve"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_curve, NGX_HTTP_VAR_CHANGEABLE, 0 }, +#ifndef OPENSSL_NO_ECH + { ngx_string("ssl_ech_status"), NULL, ngx_http_ssl_variable, + (uintptr_t) ngx_ssl_get_ech_status, NGX_HTTP_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_ech_inner_sni"), NULL, ngx_http_ssl_variable, + (uintptr_t) ngx_ssl_get_ech_inner_sni, NGX_HTTP_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_ech_outer_sni"), NULL, ngx_http_ssl_variable, + (uintptr_t) ngx_ssl_get_ech_outer_sni, NGX_HTTP_VAR_CHANGEABLE, 0 }, +#endif { ngx_string("ssl_curves"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_curves, NGX_HTTP_VAR_CHANGEABLE, 0 }, @@ -625,6 +642,9 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf) * sscf->ocsp_responder = { 0, NULL }; * sscf->stapling_file = { 0, NULL }; * sscf->stapling_responder = { 0, NULL }; + * #ifndef OPENSSL_NO_ECH + * sscf->echkeydir = { 0, NULL} ; + * #endif */ sscf->prefer_server_ciphers = NGX_CONF_UNSET; @@ -691,6 +711,9 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL); ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, ""); +#ifndef OPENSSL_NO_ECH + ngx_conf_merge_str_value(conf->echkeydir, prev->echkeydir, ""); +#endif ngx_conf_merge_str_value(conf->client_certificate, prev->client_certificate, ""); @@ -872,6 +895,12 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } +#ifndef OPENSSL_NO_ECH + if (ngx_ssl_echkeydir(cf, &conf->ssl, &conf->echkeydir) != NGX_OK) { + return NGX_CONF_ERROR; + } +#endif + if (ngx_ssl_ecdh_curve(cf, &conf->ssl, &conf->ecdh_curve) != 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 index 9b26529fa..f30c35fd0 100644 --- a/src/http/modules/ngx_http_ssl_module.h +++ b/src/http/modules/ngx_http_ssl_module.h @@ -42,6 +42,9 @@ typedef struct { ngx_ssl_cache_t *certificate_cache; ngx_str_t dhparam; +#ifndef OPENSSL_NO_ECH + ngx_str_t echkeydir; +#endif ngx_str_t ecdh_curve; ngx_str_t client_certificate; ngx_str_t trusted_certificate; From 254acb34dde0da400dd0d893903e89af8977172c Mon Sep 17 00:00:00 2001 From: sftcd Date: Sat, 6 Sep 2025 22:10:02 +0100 Subject: [PATCH 2/6] fixup! OpenSSL ECH integration --- ECH-build.md | 8 ++++++++ src/event/ngx_event_openssl.c | 9 +++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ECH-build.md b/ECH-build.md index c6eee1f4f..e7079a70b 100644 --- a/ECH-build.md +++ b/ECH-build.md @@ -109,6 +109,14 @@ publishing service parameters](https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech) designed to assist web servers in handling e.g. frequent ECH key rotation. +The `dig` example above assumes support for HTTPS RRs, for earlier +versions of `dig` one would see something like: + +`` +$ dig +short -t type65 foo.example.com +\# 165 00010000040004D56C6C65000500820080FE0D003CF700200020189E 5FD51BC7527C67CB4883B4A79CC39642FE446965A473B7AB1E3A45F3 3058000400010001000D636F7665722E6465666F2E69650000FE0D00 3C44002000201DE542C51EF072BD7250FB486E812A697130C844602F D3148347457C685B1916000400010001000D636F7665722E6465666F 2E69650000000600102A00C6C0000001160005000000000010 +``` + ## Configuration To enable ECH for an NGINX instance, configure a directory name via the diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 7aaf1f605..392ca1059 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -1573,6 +1573,11 @@ ngx_ssl_passwords_cleanup(void *data) #ifndef OPENSSL_NO_ECH + +#ifndef PATH_MAX +#define PATH_MAC 1024 +#endif + /* load key files called .ech we find in the ssl_echkeydir directory */ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) { @@ -1614,14 +1619,14 @@ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) dirname->data, den); continue; } - snprintf(privname, PATH_MAX,"%s/%s", dirname->data, den); + snprintf(privname, PATH_MAX, "%s/%s", dirname->data, den); if (!--maxkeyfiles) { /* so we don't loop forever, ever */ ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, "load_echkeys, too many private key files to check!"); ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, "load_echkeys, maxkeyfiles is hardcoded to 1024"); - return NGX_ERROR; + return NGX_ERROR; } if (stat(privname, &thestat) == 0) { BIO *in = BIO_new_file(privname, "r"); From ea757e4229706dc7074c77c8ff131fd47d3840e4 Mon Sep 17 00:00:00 2001 From: sftcd Date: Sat, 27 Sep 2025 00:16:56 +0100 Subject: [PATCH 3/6] fixup! OpenSSL ECH integration --- src/event/ngx_event_openssl.c | 71 ++++++++++++++------------ src/http/modules/ngx_http_log_module.c | 46 +++++++++++------ 2 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 392ca1059..94bc31353 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -1579,30 +1579,31 @@ ngx_ssl_passwords_cleanup(void *data) #endif /* load key files called .ech we find in the ssl_echkeydir directory */ -static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) +static int +ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) { /* 1024 private key files (maxkeyfiles) is plenty */ - int somekeyworked = 0, numkeys = 0, maxkeyfiles=1024; - char *den = NULL, *last4 = NULL; - size_t elen = dirname->len, nlen = 0; - struct stat thestat; - ngx_dir_t thedir; - ngx_int_t nrv = ngx_open_dir(dirname, &thedir); - OSSL_ECHSTORE * const es = OSSL_ECHSTORE_new(NULL, NULL); - char privname[PATH_MAX]; + int somekeyworked = 0, numkeys = 0, maxkeyfiles = 1024; + char *den = NULL, *last4 = NULL, privname[PATH_MAX]; + size_t elen = dirname->len, nlen = 0; + ngx_dir_t thedir; + ngx_int_t nrv = ngx_open_dir(dirname, &thedir); + struct stat thestat; + OSSL_ECHSTORE *const es = OSSL_ECHSTORE_new(NULL, NULL); if (es == NULL) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys, error allocating store" , __LINE__); + "ngx_load_echkeys, error allocating store" , __LINE__); return NGX_ERROR; } if (nrv != NGX_OK) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys, error opening %s at %d", dirname->data, __LINE__); + "ngx_load_echkeys, error opening %s at %d", + dirname->data, __LINE__); return NGX_ERROR; } - for (;;) { - nrv=ngx_read_dir(&thedir); + for ( ;; ) { + nrv = ngx_read_dir(&thedir); if (nrv != NGX_OK) { break; } @@ -1615,17 +1616,17 @@ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) } if ((elen + 1 + nlen + 1) >= PATH_MAX) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys, error, name too long: %s with %s", - dirname->data, den); + "ngx_load_echkeys, name too long: %s with %s", + dirname->data, den); continue; } snprintf(privname, PATH_MAX, "%s/%s", dirname->data, den); if (!--maxkeyfiles) { /* so we don't loop forever, ever */ ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "load_echkeys, too many private key files to check!"); + "ngx_load_echkeys, too many files to check!"); ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "load_echkeys, maxkeyfiles is hardcoded to 1024"); + "ngx_load_echkeys, hardcoded maxkeyfiles = 1024"); return NGX_ERROR; } if (stat(privname, &thestat) == 0) { @@ -1635,12 +1636,14 @@ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) if (in != NULL && 1 == OSSL_ECHSTORE_read_pem(es, in, is_retry_config)) { ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, - "load_echkeys, worked for: %s", privname); + "ngx_load_echkeys, worked for: %s", + privname); somekeyworked = 1; } else { ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "load_echkeys, failed for: %s",privname); + "ngx_load_echkeys, failed for: %s", + privname); } BIO_free_all(in); } @@ -1650,20 +1653,20 @@ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) if (somekeyworked == 0) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys failed for all keys but ECH configured"); + "ngx_load_echkeys loaded no keys but ECH configured"); return NGX_ERROR; } if (OSSL_ECHSTORE_num_keys(es, &numkeys) != 1) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys OSSL_ECHSTORE_num_keys failed"); + "ngx_load_echkeys OSSL_ECHSTORE_num_keys failed"); return NGX_ERROR; } ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, - "load_echkeys, total keys loaded: %d", numkeys); + "ngx_load_echkeys, total keys loaded: %d", numkeys); if (1 != SSL_CTX_set1_echstore(ssl->ctx, es)) { OSSL_ECHSTORE_free(es); ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "load_echkeys: SSL_CTX_set1_echstore failed"); + "ngx_load_echkeys: SSL_CTX_set1_echstore failed"); return NGX_ERROR; } OSSL_ECHSTORE_free(es); @@ -1671,6 +1674,7 @@ static int load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) return NGX_OK; } + ngx_int_t ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir) { @@ -1686,7 +1690,7 @@ ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir) ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); return NGX_ERROR; } - rv = load_echkeys(ssl, dir); + rv = ngx_load_echkeys(ssl, dir); if (rv != NGX_OK) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); return rv; @@ -5464,32 +5468,31 @@ ngx_ssl_get_cipher_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) ngx_int_t ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { - int echrv = SSL_ECH_STATUS_NOT_TRIED; - char *inner_sni = NULL, *outer_sni = NULL; - char buf[PATH_MAX]; + int echrv = SSL_ECH_STATUS_NOT_TRIED; + char *inner_sni = NULL, *outer_sni = NULL, buf[PATH_MAX]; echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); switch (echrv) { case SSL_ECH_STATUS_NOT_TRIED: - snprintf(buf,PATH_MAX, "not attempted"); + snprintf(buf,PATH_MAX, "NOT_TRIED"); break; case SSL_ECH_STATUS_FAILED: - snprintf(buf, PATH_MAX, "tried but failed"); + snprintf(buf, PATH_MAX, "TRIED_BUT_FAILED"); break; case SSL_ECH_STATUS_BAD_NAME: - snprintf(buf, PATH_MAX, "worked but bad name"); + snprintf(buf, PATH_MAX, "WORKED_BAD_NAME"); break; case SSL_ECH_STATUS_SUCCESS: - snprintf(buf, PATH_MAX, "success"); + snprintf(buf, PATH_MAX, "SUCCESS"); break; case SSL_ECH_STATUS_GREASE: - snprintf(buf, PATH_MAX, "GREASEd ECH"); + snprintf(buf, PATH_MAX, "GREASED"); break; case SSL_ECH_STATUS_BACKEND: - snprintf(buf, PATH_MAX, "Backend/inner ECH"); + snprintf(buf, PATH_MAX, "INNER"); break; default: - snprintf(buf, PATH_MAX, "error getting ECH status"); + snprintf(buf, PATH_MAX, "ERROR"); break; } OPENSSL_free(inner_sni); diff --git a/src/http/modules/ngx_http_log_module.c b/src/http/modules/ngx_http_log_module.c index 01b96f0c4..2413ba23e 100644 --- a/src/http/modules/ngx_http_log_module.c +++ b/src/http/modules/ngx_http_log_module.c @@ -928,33 +928,49 @@ static u_char * ngx_http_log_ech_status(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op) { - int echstat = SSL_ECH_STATUS_NOT_TRIED; - SSL *ssl = NULL; - char *sni_ech = NULL, *sni_clr = NULL, *hostheader = NULL; - u_char *sprv = NULL; - const char *str; + int echstat = SSL_ECH_STATUS_NOT_TRIED; + SSL *ssl = NULL; + char *sni_ech = NULL, *sni_clr = NULL, *hostheader = NULL; + u_char *sprv = NULL; + const char *str; /* * this is a bit oddly structured but is based on what was done for * lighttpd (by it's upstream maintainer) and what we did for haproxy - * and re-use makes us all happy + * and re-use makes us all happy */ if (!r || !r->connection || !r->connection->ssl || !r->connection->ssl->connection) return ngx_sprintf(buf, "ECH: no TLS connection"); ssl = r->connection->ssl->connection; - if (r->headers_in.server.len > 0) + if (r->headers_in.server.len > 0) hostheader = (char *)r->headers_in.server.data; #define s(x) #x switch ((echstat = SSL_ech_get1_status(ssl, &sni_ech, &sni_clr))) { - case SSL_ECH_STATUS_SUCCESS: str = s(SSL_ECH_STATUS_SUCCESS); break; - case SSL_ECH_STATUS_NOT_TRIED: str = s(SSL_ECH_STATUS_NOT_TRIED); break; - case SSL_ECH_STATUS_FAILED: str = s(SSL_ECH_STATUS_FAILED); break; - case SSL_ECH_STATUS_BAD_NAME: str = s(SSL_ECH_STATUS_BAD_NAME); break; - case SSL_ECH_STATUS_BAD_CALL: str = s(SSL_ECH_STATUS_BAD_CALL); break; - case SSL_ECH_STATUS_GREASE: str = s(SSL_ECH_STATUS_GREASE); break; - case SSL_ECH_STATUS_BACKEND: str = s(SSL_ECH_STATUS_BACKEND); break; - default: str = "ECH status unknown"; break; + case SSL_ECH_STATUS_SUCCESS: + str = s(SSL_ECH_STATUS_SUCCESS); + break; + case SSL_ECH_STATUS_NOT_TRIED: + str = s(SSL_ECH_STATUS_NOT_TRIED); + break; + case SSL_ECH_STATUS_FAILED: + str = s(SSL_ECH_STATUS_FAILED); + break; + case SSL_ECH_STATUS_BAD_NAME: + str = s(SSL_ECH_STATUS_BAD_NAME); + break; + case SSL_ECH_STATUS_BAD_CALL: + str = s(SSL_ECH_STATUS_BAD_CALL); + break; + case SSL_ECH_STATUS_GREASE: + str = s(SSL_ECH_STATUS_GREASE); + break; + case SSL_ECH_STATUS_BACKEND: + str = s(SSL_ECH_STATUS_BACKEND); + break; + default: + str = "ECH status unknown"; + break; } #undef s /* From 170eb2806e8397fb19f08b00b4598fbb386d846f Mon Sep 17 00:00:00 2001 From: sftcd Date: Sun, 12 Oct 2025 20:08:36 +0100 Subject: [PATCH 4/6] fixup! fixup! OpenSSL ECH integration --- ECH-build.md | 34 ++++-- src/event/ngx_event_openssl.c | 139 ++++++++++++------------- src/event/ngx_event_openssl.h | 14 --- src/http/modules/ngx_http_log_module.c | 83 --------------- src/http/modules/ngx_http_ssl_module.c | 15 +-- src/http/modules/ngx_http_ssl_module.h | 2 - 6 files changed, 96 insertions(+), 191 deletions(-) diff --git a/ECH-build.md b/ECH-build.md index e7079a70b..0cc8f5d61 100644 --- a/ECH-build.md +++ b/ECH-build.md @@ -21,6 +21,8 @@ here. (For more on ECH "split-mode" see the ## Build +### OpenSSL + > [!NOTE] > ECH is not yet a part of an OpenSSL release, our current goal is that ECH be > part of an OpenSSL 4.0 release in spring 2026. @@ -125,6 +127,10 @@ files. The `ssl_echkeydir` directive should be in the "http" section of an NGINX configuration as shown in the example below. All ECH PEM files in that directory that are successfully decoded will be loaded. +The NGINX instance also needs to include a virtual server that matches the +ECH `public_name` so that the ECH fallback can work. The first virtual +server in the example below does this. + ``` http { log_format withech '$remote_addr - $remote_user [$time_local] ' @@ -167,31 +173,41 @@ normal `combined` log format: ``` log_format withech '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent" "$ech_status"'; + '"$http_referer" "$http_user_agent" + "ECH: $ssl_ech_status/$ssl_server_name/$ssl_ech_outer_sni"'; access_log /var/log/nginx/access.log withech; ``` That results in log lines like the following: ``` -127.0.0.1 - - [26/Feb/2025:13:35:52 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_GREASE/foo.example.com/" -127.0.0.1 - - [26/Feb/2025:13:39:39 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_NOT_TRIED/foo.example.com/" -127.0.0.1 - - [26/Feb/2025:14:08:21 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_SUCCESS/example.com/foo.example.com" -127.0.0.1 - - [26/Feb/2025:14:09:58 +0000] "GET / HTTP/1.1" 200 494 "-" "curl/8.12.0-DEV" "ECH: SSL_ECH_STATUS_NOT_TRIED/foo.example.com/" -127.0.0.1 - - [26/Feb/2025:14:11:47 +0000] "GET / HTTP/1.1" 400 255 "-" "curl/8.12.0-DEV" "ECH: no TLS connection" +127.0.0.1 - - [12/Oct/2025:18:54:07 +0100] "GET /index.html HTTP/1.1" 200 494 "-" "-" + "ECH: GREASED/foo.example.com/-" +127.0.0.1 - - [12/Oct/2025:18:54:15 +0100] "GET /index.html HTTP/1.1" 200 486 "-" "-" + "ECH: GREASED/example.com/-" +127.0.0.1 - - [12/Oct/2025:18:54:23 +0100] "GET /index.html HTTP/1.1" 200 494 "-" "-" + "ECH: SUCCESS/foo.example.com/example.com" +127.0.0.1 - - [12/Oct/2025:18:54:31 +0100] "GET /index.html HTTP/1.1" 200 494 "-" "-" + "ECH: SUCCESS/foo.example.com/example.com" ``` When ECH has succeeded, then the outer SNI and inner SNI are included in that order. If a client GREASEd or didn't try ECH at all, and no outer SNI was provided, the HTTP host header will be shown instead. Connections that did not use TLS show that. The TLS version is not specifically shown, so TLSv1.2 -connections will show up as `SSL_ECH_STATUS_NOT_TRIED`. +connections will show up as `NOT_TRIED`. At start-up, and on configuration re-load, NGINX will log (to `error.log` at the "notice" log level) the names of ECH PEM files successfully loaded and the total number of ECH keys loaded, for each `server` stanza in the configuration. Errors in loading keys are also logged and may result in the server not -starting. +starting. Example log lines would be: + +``` +2025/10/12 18:54:07 [notice] 768265#0: ngx_ssl_load_echkeys, worked for: /etc/nginx/echkeydir/echconfig.pem.ech +2025/10/12 18:54:07 [notice] 768265#0: ngx_ssl_load_echkeys, worked for: /etc/nginx/echkeydir/d13.pem.ech +2025/10/12 18:54:07 [notice] 768265#0: ngx_ssl_load_echkeys, total keys loaded: 2 +``` ## CGI variables @@ -208,7 +224,7 @@ NGINX config: ``` fastcgi_param SSL_ECH_STATUS $ssl_ech_status; -fastcgi_param SSL_ECH_INNER_SNI $ssl_ech_inner_sni; +fastcgi_param SSL_ECH_INNER_SNI $ssl_server_name; fastcgi_param SSL_ECH_OUTER_SNI $ssl_ech_outer_sni; ``` diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 94bc31353..61f9a551e 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -14,6 +14,12 @@ #endif +/* check defines from for ECH support */ +#if !defined(SSL_OP_ECH_GREASE) && !defined(SSL_R_ECH_REJECTED) +#define OPENSSL_NO_ECH +#endif + + #define NGX_SSL_PASSWORD_BUFFER_SIZE 4096 @@ -1575,33 +1581,38 @@ ngx_ssl_passwords_cleanup(void *data) #ifndef OPENSSL_NO_ECH #ifndef PATH_MAX -#define PATH_MAC 1024 +#define PATH_MAX 1024 #endif /* load key files called .ech we find in the ssl_echkeydir directory */ -static int -ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) +static ngx_int_t +ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) { - /* 1024 private key files (maxkeyfiles) is plenty */ - int somekeyworked = 0, numkeys = 0, maxkeyfiles = 1024; - char *den = NULL, *last4 = NULL, privname[PATH_MAX]; - size_t elen = dirname->len, nlen = 0; + int somekeyworked, numkeys, maxkeyfiles; + char *den, *last4, privname[PATH_MAX]; + size_t elen, nlen; ngx_dir_t thedir; - ngx_int_t nrv = ngx_open_dir(dirname, &thedir); + ngx_int_t nrv; struct stat thestat; - OSSL_ECHSTORE *const es = OSSL_ECHSTORE_new(NULL, NULL); + OSSL_ECHSTORE *es; + es = OSSL_ECHSTORE_new(NULL, NULL); if (es == NULL) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys, error allocating store" , __LINE__); + "ngx_ssl_load_echkeys, error allocating store" ); return NGX_ERROR; } + nrv = ngx_open_dir(dirname, &thedir); if (nrv != NGX_OK) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys, error opening %s at %d", - dirname->data, __LINE__); + "ngx_ssl_load_echkeys, error opening %s", dirname->data); return NGX_ERROR; } + + somekeyworked = 0; + numkeys = 0; + maxkeyfiles = 1024; /* 1024 private key files (maxkeyfiles) is plenty */ + elen = dirname->len; for ( ;; ) { nrv = ngx_read_dir(&thedir); if (nrv != NGX_OK) { @@ -1616,7 +1627,7 @@ ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) } if ((elen + 1 + nlen + 1) >= PATH_MAX) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys, name too long: %s with %s", + "ngx_ssl_load_echkeys, name too long: %s with %s", dirname->data, den); continue; } @@ -1624,9 +1635,9 @@ ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) if (!--maxkeyfiles) { /* so we don't loop forever, ever */ ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "ngx_load_echkeys, too many files to check!"); + "ngx_ssl_load_echkeys, too many files to check!"); ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "ngx_load_echkeys, hardcoded maxkeyfiles = 1024"); + "ngx_ssl_load_echkeys, hardcoded maxkeyfiles = 1024"); return NGX_ERROR; } if (stat(privname, &thestat) == 0) { @@ -1636,13 +1647,13 @@ ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) if (in != NULL && 1 == OSSL_ECHSTORE_read_pem(es, in, is_retry_config)) { ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, - "ngx_load_echkeys, worked for: %s", + "ngx_ssl_load_echkeys, worked for: %s", privname); somekeyworked = 1; } else { ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, - "ngx_load_echkeys, failed for: %s", + "ngx_ssl_load_echkeys, failed for: %s", privname); } BIO_free_all(in); @@ -1653,51 +1664,54 @@ ngx_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) if (somekeyworked == 0) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys loaded no keys but ECH configured"); + "ngx_ssl_load_echkeys loaded no keys but ECH configured"); return NGX_ERROR; } if (OSSL_ECHSTORE_num_keys(es, &numkeys) != 1) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys OSSL_ECHSTORE_num_keys failed"); + "ngx_ssl_load_echkeys OSSL_ECHSTORE_num_keys failed"); return NGX_ERROR; } ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, - "ngx_load_echkeys, total keys loaded: %d", numkeys); + "ngx_ssl_load_echkeys, total keys loaded: %d", numkeys); if (1 != SSL_CTX_set1_echstore(ssl->ctx, es)) { OSSL_ECHSTORE_free(es); ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, - "ngx_load_echkeys: SSL_CTX_set1_echstore failed"); + "ngx_ssl_load_echkeys: SSL_CTX_set1_echstore failed"); return NGX_ERROR; } OSSL_ECHSTORE_free(es); return NGX_OK; } - +#endif ngx_int_t ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir) { - int rv = 0; - +#ifndef OPENSSL_NO_ECH if (!dir) { return NGX_OK; } if (dir->len == 0) { return NGX_OK; } - if (cf != NULL && ngx_conf_full_name(cf->cycle, dir, 1) != NGX_OK) { - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); + if (ngx_conf_full_name(cf->cycle, dir, 1) != NGX_OK) { return NGX_ERROR; } - rv = ngx_load_echkeys(ssl, dir); - if (rv != NGX_OK) { - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ECH error at %d", __LINE__); - return rv; + + if (ngx_ssl_load_echkeys(ssl, dir) != NGX_OK) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "ngx_ssl_load_echkeys error for %s", dir->data); + return NGX_ERROR; } return NGX_OK; -} +#else + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "ECH configured but not supported"); + return NGX_ERROR; #endif +} ngx_int_t @@ -5464,87 +5478,70 @@ ngx_ssl_get_cipher_name(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) } -#ifndef OPENSSL_NO_ECH ngx_int_t ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { - int echrv = SSL_ECH_STATUS_NOT_TRIED; - char *inner_sni = NULL, *outer_sni = NULL, buf[PATH_MAX]; +#ifndef OPENSSL_NO_ECH + int echrv; + char *inner_sni, *outer_sni; + inner_sni = NULL; + outer_sni = NULL; echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); switch (echrv) { case SSL_ECH_STATUS_NOT_TRIED: - snprintf(buf,PATH_MAX, "NOT_TRIED"); + ngx_str_set(s, "NOT_TRIED"); break; case SSL_ECH_STATUS_FAILED: - snprintf(buf, PATH_MAX, "TRIED_BUT_FAILED"); + ngx_str_set(s, "FAILED"); break; case SSL_ECH_STATUS_BAD_NAME: - snprintf(buf, PATH_MAX, "WORKED_BAD_NAME"); + ngx_str_set(s, "WORKED_BAD_NAME"); break; case SSL_ECH_STATUS_SUCCESS: - snprintf(buf, PATH_MAX, "SUCCESS"); + ngx_str_set(s, "SUCCESS"); break; case SSL_ECH_STATUS_GREASE: - snprintf(buf, PATH_MAX, "GREASED"); + ngx_str_set(s, "GREASED"); break; case SSL_ECH_STATUS_BACKEND: - snprintf(buf, PATH_MAX, "INNER"); + ngx_str_set(s, "INNER"); break; default: - snprintf(buf, PATH_MAX, "ERROR"); + ngx_str_set(s, "STATUS_ERROR"); break; } OPENSSL_free(inner_sni); OPENSSL_free(outer_sni); - s->len = ngx_strlen(buf); - s->data = ngx_pnalloc(pool, s->len); - ngx_memcpy(s->data, buf, s->len); - return NGX_OK; -} - -ngx_int_t -ngx_ssl_get_ech_inner_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) -{ - int echrv = SSL_ECH_STATUS_NOT_TRIED; - char *inner_sni, *outer_sni; - - echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); - if (echrv == SSL_ECH_STATUS_SUCCESS && inner_sni) { - s->len = strlen(inner_sni); - s->data = ngx_pnalloc(pool, s->len); - ngx_memcpy(s->data, inner_sni, s->len); - } else { - s->len = ngx_strlen("NONE"); - s->data = ngx_pnalloc(pool, s->len); - ngx_memcpy(s->data, "NONE", s->len); - } - OPENSSL_free(inner_sni); - OPENSSL_free(outer_sni); +#endif return NGX_OK; } ngx_int_t ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { - int echrv = SSL_ECH_STATUS_NOT_TRIED; +#ifndef OPENSSL_NO_ECH + int echrv; char *inner_sni, *outer_sni; + inner_sni = NULL; + outer_sni = NULL; echrv = SSL_ech_get1_status(c->ssl->connection, &inner_sni, &outer_sni); if (echrv == SSL_ECH_STATUS_SUCCESS && outer_sni) { - s->len = strlen(outer_sni); + s->len = ngx_strlen(outer_sni); s->data = ngx_pnalloc(pool, s->len); + if (s->data == NULL) { + return NGX_ERROR; + } ngx_memcpy(s->data, outer_sni, s->len); } else { - s->len = ngx_strlen("NONE"); - s->data = ngx_pnalloc(pool, s->len); - ngx_memcpy(s->data, "NONE", s->len); + ngx_str_set(s, ""); } OPENSSL_free(inner_sni); OPENSSL_free(outer_sni); +#endif return NGX_OK; } -#endif ngx_int_t diff --git a/src/event/ngx_event_openssl.h b/src/event/ngx_event_openssl.h index 6b3f8e816..3eb662824 100644 --- a/src/event/ngx_event_openssl.h +++ b/src/event/ngx_event_openssl.h @@ -34,14 +34,6 @@ #include #include -/* check defines from for ECH support */ -#if !defined(SSL_OP_ECH_GREASE) -#define OPENSSL_NO_ECH -#endif -#ifndef OPENSSL_NO_ECH -#include -#endif - #define NGX_SSL_NAME "OpenSSL" @@ -305,9 +297,7 @@ enum ssl_select_cert_result_t ngx_ssl_select_certificate( ngx_int_t ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags); -#ifndef OPENSSL_NO_ECH ngx_int_t ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir); -#endif void ngx_ssl_remove_cached_session(SSL_CTX *ssl, ngx_ssl_session_t *sess); ngx_int_t ngx_ssl_set_session(ngx_connection_t *c, ngx_ssl_session_t *session); @@ -337,14 +327,10 @@ ngx_int_t ngx_ssl_get_ciphers(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_curve(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); -#ifndef OPENSSL_NO_ECH ngx_int_t ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); -ngx_int_t ngx_ssl_get_ech_inner_sni(ngx_connection_t *c, ngx_pool_t *pool, - ngx_str_t *s); ngx_int_t ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); -#endif ngx_int_t ngx_ssl_get_curves(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s); ngx_int_t ngx_ssl_get_session_id(ngx_connection_t *c, ngx_pool_t *pool, diff --git a/src/http/modules/ngx_http_log_module.c b/src/http/modules/ngx_http_log_module.c index 2413ba23e..b578104c9 100644 --- a/src/http/modules/ngx_http_log_module.c +++ b/src/http/modules/ngx_http_log_module.c @@ -129,10 +129,6 @@ static u_char *ngx_http_log_body_bytes_sent(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op); static u_char *ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, ngx_http_log_op_t *op); -#ifndef OPENSSL_NO_ECH -static u_char *ngx_http_log_ech_status(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op); -#endif static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, ngx_str_t *value, ngx_uint_t escape); @@ -234,10 +230,6 @@ static ngx_str_t ngx_http_combined_fmt = "\"$http_referer\" \"$http_user_agent\""); -#ifndef OPENSSL_NO_ECH -#define NGX_ECH_STATUS_LEN 140 -#endif - static ngx_http_log_var_t ngx_http_log_vars[] = { { ngx_string("pipe"), 1, ngx_http_log_pipe }, { ngx_string("time_local"), sizeof("28/Sep/1970:12:00:00 +0600") - 1, @@ -253,11 +245,6 @@ static ngx_http_log_var_t ngx_http_log_vars[] = { ngx_http_log_body_bytes_sent }, { ngx_string("request_length"), NGX_SIZE_T_LEN, ngx_http_log_request_length }, -#ifndef OPENSSL_NO_ECH - { ngx_string("ech_status"), NGX_ECH_STATUS_LEN, - ngx_http_log_ech_status }, -#endif - { ngx_null_string, 0, NULL } }; @@ -923,76 +910,6 @@ ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, return ngx_sprintf(buf, "%O", r->request_length); } -#ifndef OPENSSL_NO_ECH -static u_char * -ngx_http_log_ech_status(ngx_http_request_t *r, u_char *buf, - ngx_http_log_op_t *op) -{ - int echstat = SSL_ECH_STATUS_NOT_TRIED; - SSL *ssl = NULL; - char *sni_ech = NULL, *sni_clr = NULL, *hostheader = NULL; - u_char *sprv = NULL; - const char *str; - - /* - * this is a bit oddly structured but is based on what was done for - * lighttpd (by it's upstream maintainer) and what we did for haproxy - * and re-use makes us all happy - */ - if (!r || !r->connection || !r->connection->ssl - || !r->connection->ssl->connection) - return ngx_sprintf(buf, "ECH: no TLS connection"); - ssl = r->connection->ssl->connection; - if (r->headers_in.server.len > 0) - hostheader = (char *)r->headers_in.server.data; -#define s(x) #x - switch ((echstat = SSL_ech_get1_status(ssl, &sni_ech, &sni_clr))) { - case SSL_ECH_STATUS_SUCCESS: - str = s(SSL_ECH_STATUS_SUCCESS); - break; - case SSL_ECH_STATUS_NOT_TRIED: - str = s(SSL_ECH_STATUS_NOT_TRIED); - break; - case SSL_ECH_STATUS_FAILED: - str = s(SSL_ECH_STATUS_FAILED); - break; - case SSL_ECH_STATUS_BAD_NAME: - str = s(SSL_ECH_STATUS_BAD_NAME); - break; - case SSL_ECH_STATUS_BAD_CALL: - str = s(SSL_ECH_STATUS_BAD_CALL); - break; - case SSL_ECH_STATUS_GREASE: - str = s(SSL_ECH_STATUS_GREASE); - break; - case SSL_ECH_STATUS_BACKEND: - str = s(SSL_ECH_STATUS_BACKEND); - break; - default: - str = "ECH status unknown"; - break; - } -#undef s - /* - * We output ECH status, then either the outer SNI or the host header (if - * outer SNI is NULL) and the inner SNI if non-NULL. - */ - if (echstat != SSL_ECH_STATUS_SUCCESS) { - OPENSSL_free(sni_clr); - sni_clr = (char *)SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); - } - if (sni_clr != NULL) - hostheader = sni_clr; - sprv = ngx_sprintf(buf, "ECH: %s/%s/%s", str, - (hostheader == NULL ? "" : hostheader), - (sni_ech == NULL ? "" : sni_ech)); - OPENSSL_free(sni_ech); - if (echstat == SSL_ECH_STATUS_SUCCESS) - OPENSSL_free(sni_clr); - return sprv; -} -#endif - static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, ngx_str_t *value, ngx_uint_t escape) diff --git a/src/http/modules/ngx_http_ssl_module.c b/src/http/modules/ngx_http_ssl_module.c index affac94e6..2f8202e85 100644 --- a/src/http/modules/ngx_http_ssl_module.c +++ b/src/http/modules/ngx_http_ssl_module.c @@ -215,14 +215,12 @@ static ngx_command_t ngx_http_ssl_commands[] = { offsetof(ngx_http_ssl_srv_conf_t, session_tickets), NULL }, -#ifndef OPENSSL_NO_ECH { ngx_string("ssl_echkeydir"), NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_SRV_CONF_OFFSET, offsetof(ngx_http_ssl_srv_conf_t, echkeydir), NULL }, -#endif { ngx_string("ssl_session_ticket_key"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, @@ -363,14 +361,12 @@ static ngx_http_variable_t ngx_http_ssl_vars[] = { { ngx_string("ssl_curve"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_curve, NGX_HTTP_VAR_CHANGEABLE, 0 }, -#ifndef OPENSSL_NO_ECH + { ngx_string("ssl_ech_status"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_ech_status, NGX_HTTP_VAR_CHANGEABLE, 0 }, - { ngx_string("ssl_ech_inner_sni"), NULL, ngx_http_ssl_variable, - (uintptr_t) ngx_ssl_get_ech_inner_sni, NGX_HTTP_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_ech_outer_sni"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_ech_outer_sni, NGX_HTTP_VAR_CHANGEABLE, 0 }, -#endif { ngx_string("ssl_curves"), NULL, ngx_http_ssl_variable, (uintptr_t) ngx_ssl_get_curves, NGX_HTTP_VAR_CHANGEABLE, 0 }, @@ -642,9 +638,7 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf) * sscf->ocsp_responder = { 0, NULL }; * sscf->stapling_file = { 0, NULL }; * sscf->stapling_responder = { 0, NULL }; - * #ifndef OPENSSL_NO_ECH * sscf->echkeydir = { 0, NULL} ; - * #endif */ sscf->prefer_server_ciphers = NGX_CONF_UNSET; @@ -711,9 +705,8 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL); ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, ""); -#ifndef OPENSSL_NO_ECH + ngx_conf_merge_str_value(conf->echkeydir, prev->echkeydir, ""); -#endif ngx_conf_merge_str_value(conf->client_certificate, prev->client_certificate, ""); @@ -895,11 +888,9 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } -#ifndef OPENSSL_NO_ECH if (ngx_ssl_echkeydir(cf, &conf->ssl, &conf->echkeydir) != NGX_OK) { return NGX_CONF_ERROR; } -#endif if (ngx_ssl_ecdh_curve(cf, &conf->ssl, &conf->ecdh_curve) != 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 index f30c35fd0..68427e009 100644 --- a/src/http/modules/ngx_http_ssl_module.h +++ b/src/http/modules/ngx_http_ssl_module.h @@ -42,9 +42,7 @@ typedef struct { ngx_ssl_cache_t *certificate_cache; ngx_str_t dhparam; -#ifndef OPENSSL_NO_ECH ngx_str_t echkeydir; -#endif ngx_str_t ecdh_curve; ngx_str_t client_certificate; ngx_str_t trusted_certificate; From 7502281252bd3d5310853174bbaccc03ae5206b7 Mon Sep 17 00:00:00 2001 From: sftcd Date: Sun, 12 Oct 2025 23:36:30 +0100 Subject: [PATCH 5/6] fixup! fixup! fixup! OpenSSL ECH integration --- ECH-build.md | 38 ++++++- src/event/ngx_event_openssl.c | 204 ++++++++++++++++++++++++++++++++-- 2 files changed, 232 insertions(+), 10 deletions(-) diff --git a/ECH-build.md b/ECH-build.md index 0cc8f5d61..537dd769d 100644 --- a/ECH-build.md +++ b/ECH-build.md @@ -55,6 +55,35 @@ $ make This results in an NGINX binary in `objs/nginx` with a statically linked OpenSSL, so as not to disturb system libraries. +### BoringSSL + +BoringSSL is also supported by curl and also supports ECH, so to build +with that, instead of our ECH-enabled OpenSSL: + +```bash + cd $HOME/code + git clone https://boringssl.googlesource.com/boringssl + cd boringssl + cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/code/boringssl/inst -DBUILD_SHARED_LIBS=1 + make + ... + make install +``` + +Then an option to build NGINX is: + +```bash +$ cd /home/user/code +$ git clone https://github.com/sftcd/nginx.git +$ cd nginx +$ ./auto/configure --prefix=nginx --with-cc-opt="-I $HOME/code/boringssl/inst/include" --with-ld-opt="-L $HOME/code//boringssl/inst/lib" --with-http_v2_module --with-http_ssl_module +$ make +...stuff... +``` + +This results in an NGINX binary in `objs/nginx` with a statically linked +OpenSSL, so as not to disturb system libraries. + ## ECH Key Generation and Publication In the remaining, we describe a configuration that uses `example.com` as the @@ -191,12 +220,15 @@ That results in log lines like the following: "ECH: SUCCESS/foo.example.com/example.com" ``` -When ECH has succeeded, then the outer SNI and inner SNI are included in that +When ECH has succeeded with OpenSSL, then the outer SNI and inner SNI are included in that order. If a client GREASEd or didn't try ECH at all, and no outer SNI was provided, the HTTP host header will be shown instead. Connections that did not use TLS show that. The TLS version is not specifically shown, so TLSv1.2 connections will show up as `NOT_TRIED`. +With BoringSSL, we don't get access to the outer SNI value, so that will +be shown as `"-'`, nor the more detailed ECH status values (only SUCCESS/FAILED). + At start-up, and on configuration re-load, NGINX will log (to `error.log` at the "notice" log level) the names of ECH PEM files successfully loaded and the total number of ECH keys loaded, for each `server` stanza in the configuration. @@ -230,6 +262,8 @@ fastcgi_param SSL_ECH_OUTER_SNI $ssl_ech_outer_sni; ## Code changes +**This section is outdated.** + - New code is protected using `#ifndef OPENSSL_NO_ECH` as is done in the OpenSSL ECH feature branch. That is set in `src/event/ngx_event_openssl.h` if the new ECH symbol `SSL_OP_ECH_GREASE` is not defined in `ssl.h`. In other @@ -288,9 +322,9 @@ When ECH PEM files are loaded or re-loaded that's logged to the error log, e.g.: ``` -2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, total keys loaded: 2 2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/echconfig.pem.ech 2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/d13.pem.ech +2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, total keys loaded: 2 ``` > [!NOTE] diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c index 61f9a551e..ea553e3da 100644 --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -19,6 +19,14 @@ #define OPENSSL_NO_ECH #endif +/* + * Boring needs us to handle ECH PEM file content directly, so we + * need to know a bit more about HPKE internals + */ +#if !defined(OPENSSL_NO_ECH) && defined(OPENSSL_IS_BORINGSSL) +#include +#define OSSL_ECH_FOR_RETRY 1 +#endif #define NGX_SSL_PASSWORD_BUFFER_SIZE 4096 @@ -1584,6 +1592,136 @@ ngx_ssl_passwords_cleanup(void *data) #define PATH_MAX 1024 #endif +#if defined(BORINGSSL_API_VERSION) +static ngx_int_t +ngx_ssl_ech_boring_read_pem(ngx_ssl_t *ssl, SSL_ECH_KEYS *keys, + const char *fname, int is_retry_config) +{ + BIO *bio; + long configlen; + u_char *config, key[32]; + size_t keylen; + EVP_PKEY *pkey; + EVP_HPKE_KEY *hpkey; + + pkey = NULL; + hpkey = NULL; + + bio = BIO_new_file((char *) fname, "r"); + if (bio == NULL) { + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + "BIO_new_file(\"%s\") failed", fname); + 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", + fname); + 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", + fname); + 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", + fname); + 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, is_retry_config, 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; + + return NGX_OK; + +failed: + + 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; + +} +#endif + /* load key files called .ech we find in the ssl_echkeydir directory */ static ngx_int_t ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) @@ -1594,14 +1732,26 @@ ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) ngx_dir_t thedir; ngx_int_t nrv; struct stat thestat; +#if !defined(BORINGSSL_API_VERSION) OSSL_ECHSTORE *es; +#else + SSL_ECH_KEYS *keys; +#endif +#if defined(BORINGSSL_API_VERSION) + keys = SSL_ECH_KEYS_new(); + if (keys == NULL) { + return NGX_ERROR; + } +#else es = OSSL_ECHSTORE_new(NULL, NULL); if (es == NULL) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ngx_ssl_load_echkeys, error allocating store" ); return NGX_ERROR; } +#endif + nrv = ngx_open_dir(dirname, &thedir); if (nrv != NGX_OK) { ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, @@ -1641,8 +1791,24 @@ ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) return NGX_ERROR; } if (stat(privname, &thestat) == 0) { - BIO *in = BIO_new_file(privname, "r"); const int is_retry_config = OSSL_ECH_FOR_RETRY; +#if defined(BORINGSSL_API_VERSION) + + if (NGX_OK == ngx_ssl_ech_boring_read_pem(ssl, keys, privname, + is_retry_config)) { + ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, + "ngx_ssl_load_echkeys, worked for: %s", + privname); + somekeyworked = 1; + numkeys++; + } + else { + ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0, + "ngx_ssl_load_echkeys, failed for: %s", + privname); + } +#else + BIO *in = BIO_new_file(privname, "r"); if (in != NULL && 1 == OSSL_ECHSTORE_read_pem(es, in, is_retry_config)) { @@ -1657,31 +1823,41 @@ ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname) privname); } BIO_free_all(in); +#endif } } } ngx_close_dir(&thedir); if (somekeyworked == 0) { - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ngx_ssl_load_echkeys loaded no keys but ECH configured"); return NGX_ERROR; } + +#if defined(BORINGSSL_API_VERSION) + if (1 != SSL_CTX_set1_ech_keys(ssl->ctx, keys)) { + SSL_ECH_KEYS_free(keys); + return NGX_ERROR; + } + SSL_ECH_KEYS_free(keys); +#else if (OSSL_ECHSTORE_num_keys(es, &numkeys) != 1) { - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + OSSL_ECHSTORE_free(es); + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ngx_ssl_load_echkeys OSSL_ECHSTORE_num_keys failed"); return NGX_ERROR; } - ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, - "ngx_ssl_load_echkeys, total keys loaded: %d", numkeys); if (1 != SSL_CTX_set1_echstore(ssl->ctx, es)) { OSSL_ECHSTORE_free(es); - ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, + ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "ngx_ssl_load_echkeys: SSL_CTX_set1_echstore failed"); return NGX_ERROR; } OSSL_ECHSTORE_free(es); - +#endif + ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0, + "ngx_ssl_load_echkeys, total keys loaded: %d", numkeys); return NGX_OK; } #endif @@ -5482,6 +5658,7 @@ ngx_int_t ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { #ifndef OPENSSL_NO_ECH +#ifndef OPENSSL_IS_BORINGSSL int echrv; char *inner_sni, *outer_sni; @@ -5513,6 +5690,14 @@ ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) } OPENSSL_free(inner_sni); OPENSSL_free(outer_sni); +#else + if (SSL_ech_accepted(c->ssl->connection)) { + ngx_str_set(s, "SUCCESS"); + } else { + ngx_str_set(s, "FAILED"); + } +#endif + #endif return NGX_OK; } @@ -5520,7 +5705,7 @@ ngx_ssl_get_ech_status(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) ngx_int_t ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) { -#ifndef OPENSSL_NO_ECH +#if !defined(OPENSSL_NO_ECH) && !defined(OPENSSL_IS_BORINGSSL) int echrv; char *inner_sni, *outer_sni; @@ -5539,6 +5724,9 @@ ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s) } OPENSSL_free(inner_sni); OPENSSL_free(outer_sni); +#else + /* boring doesn't give us the outer SNI */ + ngx_str_set(s, ""); #endif return NGX_OK; } From 812e356d82581128e263716a99f3810461b45c2d Mon Sep 17 00:00:00 2001 From: sftcd Date: Mon, 13 Oct 2025 16:44:51 +0100 Subject: [PATCH 6/6] fixup! fixup! fixup! fixup! OpenSSL ECH integration --- ECH-build.md | 74 ++++++++++++++++---------- src/http/modules/ngx_http_log_module.c | 2 + src/stream/ngx_stream_ssl_module.c | 20 +++++++ src/stream/ngx_stream_ssl_module.h | 1 + 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/ECH-build.md b/ECH-build.md index 537dd769d..16c0e3378 100644 --- a/ECH-build.md +++ b/ECH-build.md @@ -25,7 +25,7 @@ here. (For more on ECH "split-mode" see the > [!NOTE] > ECH is not yet a part of an OpenSSL release, our current goal is that ECH be -> part of an OpenSSL 4.0 release in spring 2026. +> part of an OpenSSL 4.0 release in spring 2026. There is client and server ECH code in the OpenSSL ECH feature branch at [https://github.com/openssl/openssl/tree/feature/ech](https://github.com/openssl/openssl/tree/feature/ech). @@ -47,7 +47,7 @@ Then an option to build NGINX is: $ cd /home/user/code $ git clone https://github.com/sftcd/nginx.git $ cd nginx -$ ./auto/configure --with-debug --prefix=nginx --with-http_ssl_module --with-openssl=/home/user/code/openssl-for-nginx --with-openssl-opt="--debug" --with-http_v2_module +$ ./auto/configure --with-debug --prefix=nginx --with-http_ssl_module --with-openssl=/home/user/code/openssl-for-nginx --with-openssl-opt="--debug" --with-http_v2_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module $ make ...stuff... ``` @@ -57,7 +57,7 @@ OpenSSL, so as not to disturb system libraries. ### BoringSSL -BoringSSL is also supported by curl and also supports ECH, so to build +BoringSSL is also supported by NGINX and also supports ECH, so to build with that, instead of our ECH-enabled OpenSSL: ```bash @@ -76,7 +76,7 @@ Then an option to build NGINX is: $ cd /home/user/code $ git clone https://github.com/sftcd/nginx.git $ cd nginx -$ ./auto/configure --prefix=nginx --with-cc-opt="-I $HOME/code/boringssl/inst/include" --with-ld-opt="-L $HOME/code//boringssl/inst/lib" --with-http_v2_module --with-http_ssl_module +$ ./auto/configure --prefix=nginx --with-cc-opt="-I $HOME/code/boringssl/inst/include" --with-ld-opt="-L $HOME/code//boringssl/inst/lib" --with-http_v2_module --with-http_ssl_module --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module $ make ...stuff... ``` @@ -193,6 +193,9 @@ http { } ``` +The `ssl_echkeydir` directive can also be used with the +stream module, in the same manner. + ## Logs You can log ECH status information in the normal `access.log` by adding @@ -241,6 +244,18 @@ starting. Example log lines would be: 2025/10/12 18:54:07 [notice] 768265#0: ngx_ssl_load_echkeys, total keys loaded: 2 ``` +## Testing with curl + +If you have a build of curl that supports ECH, then you can +use that. In my local test setup, the following works: + +``` +$ ~/code/curl/src/curl --ech ecl:AD7+DQA6EwAgACCJDbbP6N6GbNTQT6v9cwGtT8YUgGCpqLqiNnDnsTIAIAAEAAEAAQALZXhhbXBsZS5jb20AAA== --connect-to foo.example.com:443:localhost:5443 https://foo.example.com/index.html --cacert cadir/oe.csr -v +... +* ECH: result: status is succeeded, inner is foo.example.com, outer is example.com +... +``` + ## CGI variables We set the following variables for, e.g. PHP code: @@ -262,33 +277,38 @@ fastcgi_param SSL_ECH_OUTER_SNI $ssl_ech_outer_sni; ## Code changes -**This section is outdated.** - -- New code is protected using `#ifndef OPENSSL_NO_ECH` as is done in the - OpenSSL ECH feature branch. That is set in `src/event/ngx_event_openssl.h` if - the new ECH symbol `SSL_OP_ECH_GREASE` is not defined in `ssl.h`. In other - words, if NGINX is built using an OpenSSL version that has ECH support, then - that will be used. If the OpenSSL version doesn't have ECH then the - ECH-specific code in NGINX is compiled out. +- If the OpenSSL or BoringSSL library has ECH support, then ECH code is + compiled. That is detected if either `SSL_OP_ECH_GREASE` (OpenSSL) or + `SSL_R_ECH_REJECTED` (BoringSSL) is defined, which is checked in + `src/events/ngx_event_openssl.c`. In other words, if NGINX is built using an + OpenSSL version that has ECH support, then that will be used. If the OpenSSL + version doesn't have ECH then most of the ECH-specific code in NGINX is + compiled out. - `src/http/modules/ngx_http_ssl_module.h` and `src/http/modules/ngx_http_ssl_module.c` define the new `ssl_echkeydir` directive and the variables that become visible to e.g. PHP code. -- `load_echkeys()` in `src/event/ngx_event_openssl.c` loads ECH PEM files as +- `ngx_ssl_load_echkeys()` in `src/event/ngx_event_openssl.c` loads ECH PEM files as directed by the `ssl_echkeydir` directive, and enables shared-mode ECH decryption if some ECH keys are loaded. If `ssl_echkeydir` is set, but no keys - are loaded, that results in an error and NGINX exits. + are loaded, that results in an error and NGINX exits. Similarly, if + `ssl_echkeydir` is set, but ECH support is not available, the server will + exit. (As BoringSSL doesn't directly support the ECH PEM file format used, + `ngx_ssl_ech_boring_read_pem` does the work of OpenSSL's + `OSSL_ECHSTORE_read_pem`.) -- `ngx_ssl_get_ech_status()`, `ngx_ssl_get_ech_inner_sni()` and - `ngx_ssl_get_ech_outer_sni()` also in `src/event/ngx_event_openssl.c` provide - for setting the CGI variables mentioned above. +- `ngx_ssl_get_ech_status()` and `ngx_ssl_get_ech_outer_sni()` also in + `src/event/ngx_event_openssl.c` provide for setting the CGI variables + mentioned above. + +- Similar changes are made for the stream module in + `src/stream/ngx_stream_ssl_module.c` + and `src/stream/ngx_stream_ssl_module.h`. -- `src/http/modules/ngx_http_log_module.c` contains code to handle the new - `$ech_status` log format, mainly in the `ngx_http_log_ech_status()` function. > [!NOTE] -> `load_echkeys()` will include the public component all loaded keys in the ECH +> `ngx_ssl_load_echkeys()` will include the public component all loaded keys in the ECH > `retry-configs` in the fallback scenario. If desired, we could add a naming > convention or additional configuration setting to distinguish which to > include in `retry-configs` or not. For now, we assume that'd better be done @@ -322,9 +342,9 @@ When ECH PEM files are loaded or re-loaded that's logged to the error log, e.g.: ``` -2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/echconfig.pem.ech -2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, worked for: /home/user/lt/echkeydir/d13.pem.ech -2023/12/03 20:09:13 [notice] 273779#0: load_echkeys, total keys loaded: 2 +2023/12/03 20:09:13 [notice] 273779#0: ngx_ssl_load_echkeys, worked for: /home/user/lt/echkeydir/echconfig.pem.ech +2023/12/03 20:09:13 [notice] 273779#0: ngx_ssl_load_echkeys, worked for: /home/user/lt/echkeydir/d13.pem.ech +2023/12/03 20:09:13 [notice] 273779#0: ngx_ssl_load_echkeys, total keys loaded: 2 ``` > [!NOTE] @@ -341,7 +361,7 @@ e.g.: To run NGINX in ``gdb`` you probably want to uncomment the ``daemon off;`` and ``master_process off;`` lines in your config file. You probably also want to build with `CFLAGS="-g -O0"` to turn off optimization, and then, e.g. if you -wanted to debug into the ``load_echkeys()`` function: +wanted to debug into the ``ngx_ssl_load_echkeys()`` function: ```bash $ gdb ~/code/nginx/objs/nginx @@ -361,20 +381,20 @@ wanted to debug into the ``load_echkeys()`` function: For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from /home/user/code/nginx/objs/nginx... - (gdb) b load_echkeys + (gdb) b ngx_ssl_load_echkeys Breakpoint 1 at 0x1402e9: file src/event/ngx_event_openssl.c, line 1469. (gdb) r -c nginxmin.conf Starting program: /home/user/code/nginx/objs/nginx -c nginxmin.conf [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". - Breakpoint 1, load_echkeys (ssl=ssl@entry=0x555555db64d8, dirname=dirname@entry=0x555555db6568) + Breakpoint 1, ngx_ssl_load_echkeys (ssl=ssl@entry=0x555555db64d8, dirname=dirname@entry=0x555555db6568) at src/event/ngx_event_openssl.c:1469 1469 { (gdb) c Continuing. - Breakpoint 1, load_echkeys (ssl=ssl@entry=0x555555dbad68, dirname=dirname@entry=0x555555dbadf8) + Breakpoint 1, ngx_ssl_load_echkeys (ssl=ssl@entry=0x555555dbad68, dirname=dirname@entry=0x555555dbadf8) at src/event/ngx_event_openssl.c:1469 1469 { (gdb) c diff --git a/src/http/modules/ngx_http_log_module.c b/src/http/modules/ngx_http_log_module.c index b578104c9..f7c4bd2f5 100644 --- a/src/http/modules/ngx_http_log_module.c +++ b/src/http/modules/ngx_http_log_module.c @@ -245,6 +245,7 @@ static ngx_http_log_var_t ngx_http_log_vars[] = { ngx_http_log_body_bytes_sent }, { ngx_string("request_length"), NGX_SIZE_T_LEN, ngx_http_log_request_length }, + { ngx_null_string, 0, NULL } }; @@ -910,6 +911,7 @@ ngx_http_log_request_length(ngx_http_request_t *r, u_char *buf, return ngx_sprintf(buf, "%O", r->request_length); } + static ngx_int_t ngx_http_log_variable_compile(ngx_conf_t *cf, ngx_http_log_op_t *op, ngx_str_t *value, ngx_uint_t escape) diff --git a/src/stream/ngx_stream_ssl_module.c b/src/stream/ngx_stream_ssl_module.c index 73dfceecd..68ab5b782 100644 --- a/src/stream/ngx_stream_ssl_module.c +++ b/src/stream/ngx_stream_ssl_module.c @@ -147,6 +147,13 @@ static ngx_command_t ngx_stream_ssl_commands[] = { offsetof(ngx_stream_ssl_srv_conf_t, dhparam), NULL }, + { ngx_string("ssl_echkeydir"), + NGX_STREAM_MAIN_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_STREAM_SRV_CONF_OFFSET, + offsetof(ngx_stream_ssl_srv_conf_t, echkeydir), + NULL }, + { ngx_string("ssl_ecdh_curve"), NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, @@ -357,6 +364,12 @@ static ngx_stream_variable_t ngx_stream_ssl_vars[] = { { ngx_string("ssl_curves"), NULL, ngx_stream_ssl_variable, (uintptr_t) ngx_ssl_get_curves, NGX_STREAM_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_ech_status"), NULL, ngx_stream_ssl_variable, + (uintptr_t) ngx_ssl_get_ech_status, NGX_STREAM_VAR_CHANGEABLE, 0 }, + + { ngx_string("ssl_ech_outer_sni"), NULL, ngx_stream_ssl_variable, + (uintptr_t) ngx_ssl_get_ech_outer_sni, NGX_STREAM_VAR_CHANGEABLE, 0 }, + { ngx_string("ssl_session_id"), NULL, ngx_stream_ssl_variable, (uintptr_t) ngx_ssl_get_session_id, NGX_STREAM_VAR_CHANGEABLE, 0 }, @@ -876,6 +889,7 @@ ngx_stream_ssl_create_srv_conf(ngx_conf_t *cf) * sscf->ocsp_responder = { 0, NULL }; * sscf->stapling_file = { 0, NULL }; * sscf->stapling_responder = { 0, NULL }; + * sscf->echkeydir = { 0, NULL }; */ sscf->handshake_timeout = NGX_CONF_UNSET_MSEC; @@ -941,6 +955,8 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, ""); + ngx_conf_merge_str_value(conf->echkeydir, prev->echkeydir, ""); + ngx_conf_merge_str_value(conf->client_certificate, prev->client_certificate, ""); ngx_conf_merge_str_value(conf->trusted_certificate, @@ -1116,6 +1132,10 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) return NGX_CONF_ERROR; } + if (ngx_ssl_echkeydir(cf, &conf->ssl, &conf->echkeydir) != NGX_OK) { + return NGX_CONF_ERROR; + } + if (ngx_ssl_ecdh_curve(cf, &conf->ssl, &conf->ecdh_curve) != NGX_OK) { return NGX_CONF_ERROR; } diff --git a/src/stream/ngx_stream_ssl_module.h b/src/stream/ngx_stream_ssl_module.h index 31f138cfd..010db67b8 100644 --- a/src/stream/ngx_stream_ssl_module.h +++ b/src/stream/ngx_stream_ssl_module.h @@ -41,6 +41,7 @@ typedef struct { ngx_ssl_cache_t *certificate_cache; ngx_str_t dhparam; + ngx_str_t echkeydir; ngx_str_t ecdh_curve; ngx_str_t client_certificate; ngx_str_t trusted_certificate;