This commit is contained in:
sftcd 2025-10-13 15:45:01 +00:00 committed by GitHub
commit a0168d0bc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 846 additions and 0 deletions

403
ECH-build.md Normal file
View File

@ -0,0 +1,403 @@
# 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
### 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.
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 --with-stream --with-stream_ssl_module --with-stream_ssl_preread_module
$ make
...stuff...
```
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 NGINX 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 --with-stream --with-stream_ssl_module --with-stream_ssl_preread_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.
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
`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.
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] '
'"$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;
}
}
```
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
`$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: $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 - - [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 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.
Errors in loading keys are also logged and may result in the server not
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
```
## 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:
- ``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_server_name;
fastcgi_param SSL_ECH_OUTER_SNI $ssl_ech_outer_sni;
```
## Code changes
- 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.
- `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. 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()` 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`.
> [!NOTE]
> `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
> 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: 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]
> 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 ``ngx_ssl_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 <http://gnu.org/licenses/gpl.html>
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:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
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 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, 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, ngx_ssl_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]
```

View File

@ -14,6 +14,20 @@
#endif
/* check defines from <openssl/ssl.h> for ECH support */
#if !defined(SSL_OP_ECH_GREASE) && !defined(SSL_R_ECH_REJECTED)
#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 <openssl/hpke.h>
#define OSSL_ECH_FOR_RETRY 1
#endif
#define NGX_SSL_PASSWORD_BUFFER_SIZE 4096
@ -1572,6 +1586,310 @@ ngx_ssl_passwords_cleanup(void *data)
}
#ifndef OPENSSL_NO_ECH
#ifndef PATH_MAX
#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 <name>.ech we find in the ssl_echkeydir directory */
static ngx_int_t
ngx_ssl_load_echkeys(ngx_ssl_t *ssl, ngx_str_t *dirname)
{
int somekeyworked, numkeys, maxkeyfiles;
char *den, *last4, privname[PATH_MAX];
size_t elen, nlen;
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,
"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) {
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,
"ngx_ssl_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,
"ngx_ssl_load_echkeys, too many files to check!");
ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0,
"ngx_ssl_load_echkeys, hardcoded maxkeyfiles = 1024");
return NGX_ERROR;
}
if (stat(privname, &thestat) == 0) {
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)) {
ngx_ssl_error(NGX_LOG_NOTICE, ssl->log, 0,
"ngx_ssl_load_echkeys, worked for: %s",
privname);
somekeyworked = 1;
}
else {
ngx_ssl_error(NGX_LOG_ALERT, ssl->log, 0,
"ngx_ssl_load_echkeys, failed for: %s",
privname);
}
BIO_free_all(in);
#endif
}
}
}
ngx_close_dir(&thedir);
if (somekeyworked == 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) {
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;
}
if (1 != SSL_CTX_set1_echstore(ssl->ctx, es)) {
OSSL_ECHSTORE_free(es);
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
ngx_int_t
ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir)
{
#ifndef OPENSSL_NO_ECH
if (!dir) {
return NGX_OK;
}
if (dir->len == 0) {
return NGX_OK;
}
if (ngx_conf_full_name(cf->cycle, dir, 1) != NGX_OK) {
return NGX_ERROR;
}
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
ngx_ssl_dhparam(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *file)
{
@ -5336,6 +5654,84 @@ 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)
{
#ifndef OPENSSL_NO_ECH
#ifndef OPENSSL_IS_BORINGSSL
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:
ngx_str_set(s, "NOT_TRIED");
break;
case SSL_ECH_STATUS_FAILED:
ngx_str_set(s, "FAILED");
break;
case SSL_ECH_STATUS_BAD_NAME:
ngx_str_set(s, "WORKED_BAD_NAME");
break;
case SSL_ECH_STATUS_SUCCESS:
ngx_str_set(s, "SUCCESS");
break;
case SSL_ECH_STATUS_GREASE:
ngx_str_set(s, "GREASED");
break;
case SSL_ECH_STATUS_BACKEND:
ngx_str_set(s, "INNER");
break;
default:
ngx_str_set(s, "STATUS_ERROR");
break;
}
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;
}
ngx_int_t
ngx_ssl_get_ech_outer_sni(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s)
{
#if !defined(OPENSSL_NO_ECH) && !defined(OPENSSL_IS_BORINGSSL)
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 = 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 {
ngx_str_set(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;
}
ngx_int_t
ngx_ssl_get_ciphers(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s)
{

View File

@ -297,6 +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);
ngx_int_t ngx_ssl_echkeydir(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *dir);
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 +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);
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_outer_sni(ngx_connection_t *c, ngx_pool_t *pool,
ngx_str_t *s);
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,

View File

@ -215,6 +215,13 @@ static ngx_command_t ngx_http_ssl_commands[] = {
offsetof(ngx_http_ssl_srv_conf_t, session_tickets),
NULL },
{ 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 },
{ ngx_string("ssl_session_ticket_key"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_array_slot,
@ -355,6 +362,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 },
{ 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_outer_sni"), NULL, ngx_http_ssl_variable,
(uintptr_t) ngx_ssl_get_ech_outer_sni, NGX_HTTP_VAR_CHANGEABLE, 0 },
{ ngx_string("ssl_curves"), NULL, ngx_http_ssl_variable,
(uintptr_t) ngx_ssl_get_curves, NGX_HTTP_VAR_CHANGEABLE, 0 },
@ -625,6 +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 };
* sscf->echkeydir = { 0, NULL} ;
*/
sscf->prefer_server_ciphers = NGX_CONF_UNSET;
@ -692,6 +706,8 @@ ngx_http_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,
@ -872,6 +888,10 @@ ngx_http_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;
}

View File

@ -42,6 +42,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;

View File

@ -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;
}

View File

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