SSL: Encrypted Client Hello support.

OpenSSL
=======

- Build OpenSSL from https://github.com/sftcd/openssl/tree/feature/ech
- PR to merge into OpenSSL feature/ech branch:
  https://github.com/openssl/openssl/pull/27561

Create ECH config
-----------------

- Output is in PEM format
- Needs to be split into 2 files: config and key

```
$ openssl ech -out certs/foo.test.pem -public_name foo.test

$awk '/-----BEGIN ECHCONFIG-----/ {f=1} /-----END ECHCONFIG-----/
     {print; f=0} f'  certs/foo.test.pem > certs/foo.test.ech

$ awk '/-----BEGIN PRIVATE KEY-----/ {f=1} /-----END PRIVATE KEY-----/
     {print; f=0} f' certs/foo.test.pem > certs/foo.test.key
```

Configuration
-------------
```
server {
    listen 8443 ssl;

    # PEM files
    ssl_echconfig       certs/foo.test.ech;
    ssl_echconfig_key   certs/foo.test.key;

    ssl_certificate     certs/example.com.crt;
    ssl_certificate_key certs/example.com.key;

    location / {
        return 200 foo;
    }
}
```

BoringSSL
=========

Create ECH config
-----------------

- All output is binary

```
$ bssl generate-ech -public-name bar.test
                    -out-ech-config-list certs/bar.test.echlist
                    -out-ech-config certs/bar.test.ech
                    -out-private-key certs/bar.test.key
                    -config-id 0
```

Configuration
-------------
```
server {
    listen 8443 ssl;

    # Binary files
    ssl_echconfig       certs/bar.test.ech;
    ssl_echconfig_key   certs/bar.test.key;

    ssl_certificate     certs/example.com.crt;
    ssl_certificate_key certs/example.com.key;

    location / {
        return 200 foo;
    }
}
```

Testing
=======

- Using OpenSSL client

OpenSSL to OpenSSL
------------------
```
$ openssl s_client -connect example.com:8443
                   -ech_config_list <base64 from certs/foo.test.ech>
                   -CAfile certs/root.crt -trace
```

OpenSSL to BoringSSL
--------------------

- Mandatory TlS 1.3

```
$ openssl s_client -connect example.com:8443
                   -ech_config_list <base64 of certs/bar.test.echlist>
                   -CAfile certs/root.crt -trace
                   -tls1_3
```

Client ECH trace
----------------

- "example.com" is encrypted, "foo.test" is open
```
...
extension_type=server_name(0), length=13
  0000 - 00 0b 00 00 08 66 6f 6f-2e 74 65 73 74         .....foo.test
extension_type=encrypted_client_hello(65037), length=218
  0000 - 00 00 01 00 01 0d 00 20-77 ac f1 94 2e 80 23   ....... w.....#
  000f - 34 18 17 61 8e 46 e7 5c-09 c5 2b 68 d7 e6 cd   4..a.F.\..+h...
  001e - 0c 35 c2 f6 41 09 2e ad-39 24 00 b0 bf 67 66   .5..A...9$...gf
  002d - fb b4 83 a8 8d 64 49 0f-fb 1c aa e9 d6 44 cc   .....dI......D.
  003c - 59 7e 59 55 7f bf d2 0f-72 e9 bf 8f 12 98 73   Y~YU....r.....s
  004b - 39 e9 8d f4 bf 87 72 ae-f5 9c e2 d9 d3 94 29   9.....r.......)
  005a - 13 f5 18 c6 92 48 b4 b7-00 5b 5b 82 b0 da f9   .....H...[[....
  0069 - 5e 26 73 75 fb 38 e9 82-3f 6b a4 ea 40 7c 77   ^&su.8..?k..@|w
  0078 - 46 99 22 a1 36 7d 8b 99-99 18 9b 4b aa 56 e6   F.".6}.....K.V.
  0087 - bd 78 f0 c6 02 f4 7b 38-e8 f5 b2 04 32 66 c6   .x....{8....2f.
  0096 - a0 e4 22 fc c3 5b ff d8-38 2a fd 0d 38 00 12   .."..[..8*..8..
  00a5 - 74 0c 91 d8 79 21 6b ad-1e 73 87 7a 39 b6 7c   t...y!k..s.z9.|
  00b4 - 38 80 a4 03 51 48 bf 47-6e 8e cc d1 17 a5 0a   8...QH.Gn......
  00c3 - 3e 28 c9 a8 4b 61 ad bb-7b 4d 97 fe a4 58 2d   >(..Ka..{M...X-
  00d2 - f7 34 b1 b8 45 74 cf 5e-                       .4..Et.^
...
```
- nginx debug log:
```
...
2025/06/06 13:45:28 [debug] 2867633#0: *1 SSL server name: "example.com"
...
```

TODO
====

- formats: PEM vs binary
- variables (dynamic loading)
- proxy
- Stream + split mode
This commit is contained in:
Roman Arutyunyan 2024-11-26 11:14:21 +04:00
parent 5b8a5c08ce
commit 6950d689f4
4 changed files with 386 additions and 0 deletions

View File

@ -9,6 +9,10 @@
#include <ngx_core.h>
#include <ngx_event.h>
#ifdef SSL_R_ECH_REJECTED
#include <openssl/hpke.h>
#endif
#define NGX_SSL_PASSWORD_BUFFER_SIZE 4096
@ -18,6 +22,17 @@ typedef struct {
} ngx_openssl_conf_t;
#ifdef OSSL_ECH_CURRENT_VERSION
static ngx_int_t ngx_ssl_echconfig(ngx_conf_t *cf, ngx_ssl_t *ssl,
OSSL_ECHSTORE *es, ngx_str_t *conf, ngx_str_t *key, ngx_array_t *passwords,
ngx_uint_t for_retry);
#elif defined (SSL_R_ECH_REJECTED)
static ngx_int_t
ngx_ssl_echconfig(ngx_conf_t *cf, ngx_ssl_t *ssl, SSL_ECH_KEYS *keys,
ngx_str_t *conf, ngx_str_t *key, ngx_uint_t for_retry);
static ngx_int_t ngx_ssl_echconfig_read(ngx_conf_t *cf, ngx_str_t *file,
ngx_str_t *value);
#endif
static ngx_inline ngx_int_t ngx_ssl_cert_already_in_hash(void);
static int ngx_ssl_verify_callback(int ok, X509_STORE_CTX *x509_store);
static void ngx_ssl_info_callback(const ngx_ssl_conn_t *ssl_conn, int where,
@ -659,6 +674,304 @@ retry:
}
#ifdef OSSL_ECH_CURRENT_VERSION
/* OpenSSL */
ngx_int_t
ngx_ssl_echconfigs(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *confs,
ngx_array_t *keys, ngx_array_t *passwords, ngx_uint_t for_retry)
{
ngx_str_t *conf, *key;
ngx_uint_t i;
OSSL_ECHSTORE *es;
es = OSSL_ECHSTORE_new(NULL, NULL);
if (es == NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "OSSL_ECHSTORE_new() failed");
return NGX_ERROR;
}
conf = confs->elts;
key = keys->elts;
for (i = 0; i < confs->nelts; i++) {
if (ngx_ssl_echconfig(cf, ssl, es, &conf[i], &key[i], passwords,
for_retry)
!= NGX_OK)
{
OSSL_ECHSTORE_free(es);
return NGX_ERROR;
}
}
if (SSL_CTX_set1_echstore(ssl->ctx, es) == 0) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"SSL_CTX_set1_echstore() failed");
OSSL_ECHSTORE_free(es);
return NGX_ERROR;
}
OSSL_ECHSTORE_free(es);
return NGX_OK;
}
static ngx_int_t
ngx_ssl_echconfig(ngx_conf_t *cf, ngx_ssl_t *ssl, OSSL_ECHSTORE *es,
ngx_str_t *conf, ngx_str_t *key, ngx_array_t *passwords,
ngx_uint_t for_retry)
{
BIO *bio;
char *err;
EVP_PKEY *pkey;
pkey = ngx_ssl_cache_fetch(cf, NGX_SSL_CACHE_PKEY, &err, key, passwords);
if (pkey == NULL) {
if (err != NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"cannot load certificate key \"%s\": %s",
key->data, err);
}
return NGX_ERROR;
}
if (ngx_strncmp(conf->data, "data:", sizeof("data:") - 1) == 0) {
bio = BIO_new_mem_buf(conf->data + sizeof("data:") - 1,
conf->len - (sizeof("data:") - 1));
if (bio == NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"BIO_new_mem_buf(\"%V\") failed", conf);
EVP_PKEY_free(pkey);
return NGX_ERROR;
}
} else {
if (ngx_get_full_name(cf->pool, (ngx_str_t *) &ngx_cycle->conf_prefix,
conf)
!= NGX_OK)
{
return NGX_ERROR;
}
bio = BIO_new_file((char *) conf->data, "r");
if (bio == NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"BIO_new_file(\"%V\") failed", conf);
EVP_PKEY_free(pkey);
return NGX_ERROR;
}
}
if (OSSL_ECHSTORE_set1_key_and_read_pem(es, pkey, bio, for_retry) == 0) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"OSSL_ECHSTORE_set1_key_and_read_pem(\"%V\") failed",
conf);
BIO_free(bio);
EVP_PKEY_free(pkey);
return NGX_ERROR;
}
BIO_free(bio);
EVP_PKEY_free(pkey);
return NGX_OK;
}
#elif defined (SSL_R_ECH_REJECTED)
/* BoringSSL */
ngx_int_t
ngx_ssl_echconfigs(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *confs,
ngx_array_t *keys, ngx_array_t *passwords, ngx_uint_t for_retry)
{
ngx_str_t *conf, *key;
ngx_uint_t i;
SSL_ECH_KEYS *ech_keys;
ech_keys = SSL_ECH_KEYS_new();
if (keys == NULL) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"SSL_ECH_KEYS_new() failed");
return NGX_ERROR;
}
conf = confs->elts;
key = keys->elts;
for (i = 0; i < confs->nelts; i++) {
if (ngx_ssl_echconfig(cf, ssl, ech_keys, &conf[i], &key[i], for_retry)
!= NGX_OK)
{
SSL_ECH_KEYS_free(ech_keys);
return NGX_ERROR;
}
}
if (SSL_CTX_set1_ech_keys(ssl->ctx, ech_keys) == 0) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
"SSL_CTX_set1_ech_keys() failed");
SSL_ECH_KEYS_free(ech_keys);
return NGX_ERROR;
}
SSL_ECH_KEYS_free(ech_keys);
return NGX_OK;
}
static ngx_int_t
ngx_ssl_echconfig(ngx_conf_t *cf, ngx_ssl_t *ssl, SSL_ECH_KEYS *keys,
ngx_str_t *conf, ngx_str_t *key, ngx_uint_t for_retry)
{
u_char *conf_data, *key_data;
size_t conf_len, key_len;
ngx_str_t value;
EVP_HPKE_KEY *hpke_key;
if (ngx_strncmp(conf->data, "data:", sizeof("data:") - 1) == 0) {
conf_data = conf->data + sizeof("data:") - 1;
conf_len = conf->len - sizeof("data:") - 1;
} else {
if (ngx_ssl_echconfig_read(cf, conf, &value) != NGX_OK) {
return NGX_ERROR;
}
conf_data = value.data;
conf_len = value.len;
}
if (ngx_strncmp(key->data, "data:", sizeof("data:") - 1) == 0) {
key_data = key->data + sizeof("data:") - 1;
key_len = key->len - sizeof("data:") - 1;
} else {
if (ngx_ssl_echconfig_read(cf, key, &value) != NGX_OK) {
return NGX_ERROR;
}
key_data = value.data;
key_len = value.len;
}
hpke_key = EVP_HPKE_KEY_new();
if (hpke_key == NULL) {
ngx_explicit_memzero(key_data, key_len);
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "EVP_HPKE_KEY_new() failed");
return NGX_ERROR;
}
if (EVP_HPKE_KEY_init(hpke_key, EVP_hpke_x25519_hkdf_sha256(),
key_data, key_len) == 0)
{
ngx_explicit_memzero(key_data, key_len);
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "EVP_HPKE_KEY_init() failed");
EVP_HPKE_KEY_free(hpke_key);
return NGX_ERROR;
}
ngx_explicit_memzero(key->data, key->len);
if (SSL_ECH_KEYS_add(keys, for_retry, conf_data, conf_len, hpke_key) == 0) {
ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0, "SSL_ECH_KEYS_add() failed");
EVP_HPKE_KEY_free(hpke_key);
return NGX_ERROR;
}
EVP_HPKE_KEY_free(hpke_key);
return NGX_OK;
}
static ngx_int_t
ngx_ssl_echconfig_read(ngx_conf_t *cf, ngx_str_t *name, ngx_str_t *value)
{
u_char *p;
size_t size;
ssize_t n;
ngx_file_t file;
ngx_file_info_t fi;
if (ngx_get_full_name(cf->pool, (ngx_str_t *) &ngx_cycle->conf_prefix, name)
!= NGX_OK)
{
return NGX_ERROR;
}
ngx_memzero(&file, sizeof(ngx_file_t));
file.name = *name;
file.log = cf->log;
file.fd = ngx_open_file(name->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
if (file.fd == NGX_INVALID_FILE) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
ngx_open_file_n " \"%V\" failed", name);
return NGX_ERROR;
}
if (ngx_fd_info(file.fd, &fi) == NGX_FILE_ERROR) {
ngx_conf_log_error(NGX_LOG_CRIT, cf, ngx_errno,
ngx_fd_info_n " \"%V\" failed", name);
goto failed;
}
size = ngx_file_size(&fi);
p = ngx_pnalloc(cf->pool, size);
if (p == NULL) {
goto failed;
}
n = ngx_read_file(&file, p, size, 0);
if (n == NGX_ERROR) {
ngx_conf_log_error(NGX_LOG_CRIT, cf, ngx_errno,
ngx_read_file_n " \"%V\" failed", &file.name);
goto failed;
}
if ((size_t) n != size) {
ngx_explicit_memzero(p, size);
ngx_conf_log_error(NGX_LOG_CRIT, cf, 0,
ngx_read_file_n " \"%V\" returned only "
"%z bytes instead of %uz", &file.name, n, size);
goto failed;
}
if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
ngx_close_file_n " \"%V\" failed", &file.name);
}
value->data = p;
value->len = size;
return NGX_OK;
failed:
if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, cf->log, ngx_errno,
ngx_close_file_n " \"%V\" failed", &file.name);
}
return NGX_ERROR;
}
#endif
ngx_int_t
ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers,
ngx_uint_t prefer_server_ciphers)

View File

@ -232,6 +232,12 @@ ngx_int_t ngx_ssl_connection_certificate(ngx_connection_t *c, ngx_pool_t *pool,
ngx_str_t *cert, ngx_str_t *key, ngx_ssl_cache_t *cache,
ngx_array_t *passwords);
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
ngx_int_t ngx_ssl_echconfigs(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *confs,
ngx_array_t *keys, ngx_array_t *passwords, ngx_uint_t for_retry);
#endif
ngx_int_t ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers,
ngx_uint_t prefer_server_ciphers);
ngx_int_t ngx_ssl_client_certificate(ngx_conf_t *cf, ngx_ssl_t *ssl,

View File

@ -117,6 +117,29 @@ static ngx_command_t ngx_http_ssl_commands[] = {
0,
NULL },
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
{ ngx_string("ssl_echconfig"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_array_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_ssl_srv_conf_t, echconfigs),
NULL },
{ ngx_string("ssl_echconfig_key"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_conf_set_str_array_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_ssl_srv_conf_t, echconfig_keys),
NULL },
{ ngx_string("ssl_echconfig_retry"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_SRV_CONF_OFFSET,
offsetof(ngx_http_ssl_srv_conf_t, echconfig_retry),
NULL },
#endif
{ ngx_string("ssl_password_file"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
ngx_http_ssl_password_file,
@ -629,6 +652,11 @@ ngx_http_ssl_create_srv_conf(ngx_conf_t *cf)
sscf->certificates = NGX_CONF_UNSET_PTR;
sscf->certificate_keys = NGX_CONF_UNSET_PTR;
sscf->certificate_cache = NGX_CONF_UNSET_PTR;
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
sscf->echconfigs = NGX_CONF_UNSET_PTR;
sscf->echconfig_keys = NGX_CONF_UNSET_PTR;
sscf->echconfig_retry = NGX_CONF_UNSET;
#endif
sscf->passwords = NGX_CONF_UNSET_PTR;
sscf->conf_commands = NGX_CONF_UNSET_PTR;
sscf->builtin_session_cache = NGX_CONF_UNSET;
@ -677,6 +705,12 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
ngx_conf_merge_ptr_value(conf->certificate_cache, prev->certificate_cache,
NULL);
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
ngx_conf_merge_ptr_value(conf->echconfigs, prev->echconfigs, NULL);
ngx_conf_merge_ptr_value(conf->echconfig_keys, prev->echconfig_keys, NULL);
ngx_conf_merge_value(conf->echconfig_retry, prev->echconfig_retry, 1);
#endif
ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL);
ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, "");
@ -724,6 +758,21 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
return NGX_CONF_OK;
}
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
if (conf->echconfigs) {
if (conf->echconfig_keys == NULL
|| conf->echconfig_keys->nelts < conf->echconfigs->nelts)
{
ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
"no \"ssl_echconfig_key\" is defined "
"for ECH configuration \"%V\"",
((ngx_str_t *) conf->echconfigs->elts)
+ conf->echconfigs->nelts - 1);
return NGX_CONF_ERROR;
}
}
#endif
if (ngx_ssl_create(&conf->ssl, conf->protocols, conf) != NGX_OK) {
return NGX_CONF_ERROR;
}
@ -794,6 +843,18 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
}
}
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
if (conf->echconfigs) {
if (ngx_ssl_echconfigs(cf, &conf->ssl, conf->echconfigs,
conf->echconfig_keys, conf->passwords,
conf->echconfig_retry)
!= NGX_OK)
{
return NGX_CONF_ERROR;
}
}
#endif
conf->ssl.buffer_size = conf->buffer_size;
if (conf->verify) {

View File

@ -40,6 +40,12 @@ typedef struct {
ngx_ssl_cache_t *certificate_cache;
#if (defined OSSL_ECH_CURRENT_VERSION || defined SSL_R_ECH_REJECTED)
ngx_array_t *echconfigs;
ngx_array_t *echconfig_keys;
ngx_flag_t echconfig_retry;
#endif
ngx_str_t dhparam;
ngx_str_t ecdh_curve;
ngx_str_t client_certificate;