]> git.kaiwu.me - nginx.git/commitdiff
Add basic ECH shared-mode via OpenSSL.
authorsftcd <stephen.farrell@cs.tcd.ie>
Wed, 26 Nov 2025 14:12:07 +0000 (14:12 +0000)
committerRoman Arutyunyan <arutyunyan.roman@gmail.com>
Mon, 1 Dec 2025 12:33:40 +0000 (16:33 +0400)
src/event/ngx_event_openssl.c
src/event/ngx_event_openssl.h
src/http/modules/ngx_http_ssl_module.c
src/http/modules/ngx_http_ssl_module.h
src/stream/ngx_stream_ssl_module.c
src/stream/ngx_stream_ssl_module.h

index a5af0ed3d98a4db42d13ce7070127bf329ccf521..fe2302966320049c48042042ab8a181b3f54d169 100644 (file)
@@ -1653,6 +1653,102 @@ ngx_ssl_dhparam(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *file)
 }
 
 
+ngx_int_t
+ngx_ssl_ech_files(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_array_t *filenames)
+{
+#ifdef SSL_OP_ECH_GREASE
+    int             numkeys;
+    BIO            *in;
+    ngx_int_t       rc;
+    ngx_str_t      *filename;
+    ngx_uint_t      i;
+    OSSL_ECHSTORE  *es;
+
+    if (filenames == NULL) {
+        return NGX_OK;
+    }
+
+    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;
+    }
+
+    rc = NGX_ERROR;
+    filename = filenames->elts;
+
+    for (i = 0; i < filenames->nelts; i++) {
+
+        if (ngx_conf_full_name(cf->cycle, &filename[i], 1) != NGX_OK) {
+            goto cleanup;
+        }
+
+        in = BIO_new_file((char *) filename[i].data, "r");
+        if (in == NULL) {
+            ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                          "BIO_new_file(\"%s\") failed", filename[i].data);
+            goto cleanup;
+        }
+
+        /*
+         * We only set the ECHConfigList from the first file read to use
+         * in ECH retry-configs.
+         *
+         * That allows many sensible key rotation schemes so that the
+         * values sent in ECH retry-configs are smaller and current.
+         * For example, if the first file name has the current ECH
+         * private key, and a second one has the previously used key
+         * that some clients may still use due to DNS caching.
+         */
+
+         if (OSSL_ECHSTORE_read_pem(es, in, i ? OSSL_ECH_NO_RETRY
+                                              : OSSL_ECH_FOR_RETRY)
+             != 1)
+         {
+             ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                           "OSSL_ECHSTORE_read_pem(%s) failed",
+                           filename[i].data);
+             BIO_free(in);
+             goto cleanup;
+         }
+
+         BIO_free(in);
+    }
+
+    /*
+     * load the ECH store after checking there's at least one ECH
+     * private key in there (the PEM file spec allows zero or one
+     * private key per file)
+     */
+
+    if (OSSL_ECHSTORE_num_keys(es, &numkeys) != 1) {
+        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                      "OSSL_ECHSTORE_num_keys(%s) failed");
+        goto cleanup;
+    }
+
+    if (numkeys > 0 && SSL_CTX_set1_echstore(ssl->ctx, es) != 1) {
+        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                      "SSL_CTX_set1_echstore() failed");
+        goto cleanup;
+    }
+
+    rc = NGX_OK;
+
+cleanup:
+
+    OSSL_ECHSTORE_free(es);
+    return rc;
+
+#else
+    ngx_log_error(NGX_LOG_WARN, ssl->log, 0,
+                  "\"ssl_ech_file\" is not supported on this platform, "
+                  "ignored");
+    return NGX_OK;
+#endif
+}
+
+
 ngx_int_t
 ngx_ssl_ecdh_curve(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *name)
 {
@@ -5708,6 +5804,81 @@ ngx_ssl_get_alpn_protocol(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)
+{
+#ifdef SSL_OP_ECH_GREASE
+    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_SUCCESS:
+        ngx_str_set(s, "SUCCESS");
+        break;
+    case SSL_ECH_STATUS_GREASE:
+        ngx_str_set(s, "GREASE");
+        break;
+    case SSL_ECH_STATUS_BACKEND:
+        ngx_str_set(s, "BACKEND");
+        break;
+    default:
+        ngx_str_set(s, "FAILED");
+        break;
+    }
+
+    OPENSSL_free(inner_sni);
+    OPENSSL_free(outer_sni);
+#else
+    s->len = 0;
+#endif
+    return NGX_OK;
+}
+
+
+ngx_int_t
+ngx_ssl_get_ech_outer_server_name(ngx_connection_t *c, ngx_pool_t *pool,
+    ngx_str_t *s)
+{
+#if defined(SSL_OP_ECH_GREASE)
+    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 {
+        s->len = 0;
+    }
+
+    OPENSSL_free(inner_sni);
+    OPENSSL_free(outer_sni);
+#else
+    s->len = 0;
+#endif
+    return NGX_OK;
+}
+
+
 ngx_int_t
 ngx_ssl_get_raw_certificate(ngx_connection_t *c, ngx_pool_t *pool, ngx_str_t *s)
 {
index a156c4bb9adcd69d6fd68b006c9c24329b2d4b0e..d86ffb8da3a5c52feb5bb49905f72be1e12d3ef9 100644 (file)
@@ -271,6 +271,8 @@ ngx_array_t *ngx_ssl_read_password_file(ngx_conf_t *cf, ngx_str_t *file);
 ngx_array_t *ngx_ssl_preserve_passwords(ngx_conf_t *cf,
     ngx_array_t *passwords);
 ngx_int_t ngx_ssl_dhparam(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *file);
+ngx_int_t ngx_ssl_ech_files(ngx_conf_t *cf, ngx_ssl_t *ssl,
+    ngx_array_t *filename);
 ngx_int_t ngx_ssl_ecdh_curve(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *name);
 ngx_int_t ngx_ssl_early_data(ngx_conf_t *cf, ngx_ssl_t *ssl,
     ngx_uint_t enable);
@@ -338,6 +340,10 @@ ngx_int_t ngx_ssl_get_early_data(ngx_connection_t *c, ngx_pool_t *pool,
     ngx_str_t *s);
 ngx_int_t ngx_ssl_get_server_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);
+ngx_int_t ngx_ssl_get_ech_outer_server_name(ngx_connection_t *c,
+    ngx_pool_t *pool, ngx_str_t *s);
 ngx_int_t ngx_ssl_get_alpn_protocol(ngx_connection_t *c, ngx_pool_t *pool,
     ngx_str_t *s);
 ngx_int_t ngx_ssl_get_raw_certificate(ngx_connection_t *c, ngx_pool_t *pool,
index c71a5de0829f7c2ae4ef48d8f3c3b76ead462194..43fcafd50e6326bfc7fe9c1790b80b1641b8c1b3 100644 (file)
@@ -117,6 +117,13 @@ static ngx_command_t  ngx_http_ssl_commands[] = {
       0,
       NULL },
 
+    { ngx_string("ssl_ech_file"),
+      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, ech_files),
+      NULL },
+
     { ngx_string("ssl_password_file"),
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1,
       ngx_http_ssl_password_file,
@@ -377,6 +384,13 @@ static ngx_http_variable_t  ngx_http_ssl_vars[] = {
     { ngx_string("ssl_alpn_protocol"), NULL, ngx_http_ssl_variable,
       (uintptr_t) ngx_ssl_get_alpn_protocol, 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_server_name"), NULL, ngx_http_ssl_variable,
+      (uintptr_t) ngx_ssl_get_ech_outer_server_name,
+      NGX_HTTP_VAR_CHANGEABLE, 0 },
+
     { ngx_string("ssl_client_cert"), NULL, ngx_http_ssl_variable,
       (uintptr_t) ngx_ssl_get_certificate, NGX_HTTP_VAR_CHANGEABLE, 0 },
 
@@ -643,6 +657,7 @@ 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;
+    sscf->ech_files = NGX_CONF_UNSET_PTR;
     sscf->passwords = NGX_CONF_UNSET_PTR;
     sscf->conf_commands = NGX_CONF_UNSET_PTR;
     sscf->builtin_session_cache = NGX_CONF_UNSET;
@@ -694,6 +709,8 @@ 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);
 
+    ngx_conf_merge_ptr_value(conf->ech_files, prev->ech_files, NULL);
+
     ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL);
 
     ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, "");
@@ -880,6 +897,10 @@ ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
         return NGX_CONF_ERROR;
     }
 
+    if (ngx_ssl_ech_files(cf, &conf->ssl, conf->ech_files) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     if (ngx_ssl_ecdh_curve(cf, &conf->ssl, &conf->ecdh_curve) != NGX_OK) {
         return NGX_CONF_ERROR;
     }
index 9b26529fa98349c4a937d1a364f292035b75ee1b..a078d44f8f3938fd5d0555689b87a7298f602351 100644 (file)
@@ -49,6 +49,7 @@ typedef struct {
 
     ngx_str_t                       ciphers;
 
+    ngx_array_t                    *ech_files;
     ngx_array_t                    *passwords;
     ngx_array_t                    *conf_commands;
 
index 6a5160f277fc64efda7fc29fa46f85030765b037..b7e5db449541147c0db7a62ff0584801ea42a890 100644 (file)
@@ -126,6 +126,13 @@ static ngx_command_t  ngx_stream_ssl_commands[] = {
       0,
       NULL },
 
+    { ngx_string("ssl_ech_file"),
+      NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_str_array_slot,
+      NGX_STREAM_SRV_CONF_OFFSET,
+      offsetof(ngx_stream_ssl_srv_conf_t, ech_files),
+      NULL },
+
     { ngx_string("ssl_password_file"),
       NGX_STREAM_MAIN_CONF|NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1,
       ngx_stream_ssl_password_file,
@@ -372,6 +379,13 @@ static ngx_stream_variable_t  ngx_stream_ssl_vars[] = {
     { ngx_string("ssl_alpn_protocol"), NULL, ngx_stream_ssl_variable,
       (uintptr_t) ngx_ssl_get_alpn_protocol, 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_server_name"), NULL, ngx_stream_ssl_variable,
+      (uintptr_t) ngx_ssl_get_ech_outer_server_name,
+      NGX_STREAM_VAR_CHANGEABLE, 0 },
+
     { ngx_string("ssl_client_cert"), NULL, ngx_stream_ssl_variable,
       (uintptr_t) ngx_ssl_get_certificate, NGX_STREAM_VAR_CHANGEABLE, 0 },
 
@@ -888,6 +902,7 @@ ngx_stream_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;
+    sscf->ech_files = NGX_CONF_UNSET_PTR;
     sscf->passwords = NGX_CONF_UNSET_PTR;
     sscf->conf_commands = NGX_CONF_UNSET_PTR;
     sscf->prefer_server_ciphers = NGX_CONF_UNSET;
@@ -943,6 +958,8 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
     ngx_conf_merge_ptr_value(conf->certificate_cache, prev->certificate_cache,
                          NULL);
 
+    ngx_conf_merge_ptr_value(conf->ech_files, prev->ech_files, NULL);
+
     ngx_conf_merge_ptr_value(conf->passwords, prev->passwords, NULL);
 
     ngx_conf_merge_str_value(conf->dhparam, prev->dhparam, "");
@@ -1124,6 +1141,10 @@ ngx_stream_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
         return NGX_CONF_ERROR;
     }
 
+    if (ngx_ssl_ech_files(cf, &conf->ssl, conf->ech_files) != NGX_OK) {
+        return NGX_CONF_ERROR;
+    }
+
     if (ngx_ssl_ecdh_curve(cf, &conf->ssl, &conf->ecdh_curve) != NGX_OK) {
         return NGX_CONF_ERROR;
     }
index 31f138cfd85048bc8f81b311dcd2fc18531f64ff..6fdd8f88cdfe0ee8f5f775b1ecef068967ffac17 100644 (file)
@@ -49,6 +49,7 @@ typedef struct {
 
     ngx_str_t         ciphers;
 
+    ngx_array_t      *ech_files;
     ngx_array_t      *passwords;
     ngx_array_t      *conf_commands;