]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
mod_ssl: Add support for Encrypted Client Hello (ECH) based off
authorJoe Orton <jorton@apache.org>
Fri, 12 Sep 2025 08:05:11 +0000 (08:05 +0000)
committerJoe Orton <jorton@apache.org>
Fri, 12 Sep 2025 08:05:11 +0000 (08:05 +0000)
proposed OpenSSL 4.0 API. Notes from PR #551:

This build only supports ECH "shared-mode" where mod_ssl does the ECH
decryption and also hosts both the ECH `public-name` and `backend` web
sites.

## 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 apache2 therefore requires building from source, using
the OpenSSL ECH feature branch.

## Code changes

- All code changes are within `modules/ssl` and are protected via `#ifdef
  HAVE_OPENSSL_ECH`.  That's defined in `ssl_private.h` if the included
`ssl.h` defines `SSL_OP_ECH_GREASE`.

- There're a bunch of changes to add the new `SSLECHKeyDir` directive that
  are mosly obvious.

- We load the keys from `SSLECHKeyDir` using the `load_echkeys()` function in
  `ssl_engine_init.c`. That also ECH-enables the `SSL_CTX` when keys are
  loaded, which triggers ECH decryption as needed.

> [!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.)

- We add a callback to `SSL_CTX_ech_set_callback` also in `ssl_engine_init.c`.

- We add calls to set the `SSL_ECH_STATUS` etc. variables to the environment
(for PHP etc) in `ssl_engine_kernel.c` and also do the logging of ECH outcomes
(to the error log).

Submitted by: sftcd <stephen.farrell cs.tcd.ie>, rpluem
Github: closes #551

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1928357 13f79535-47bb-0310-9956-ffa450edef68

.github/workflows/linux.yml
changes-entries/ech.txt [new file with mode: 0644]
docs/log-message-tags/next-number
docs/manual/mod/mod_ssl.xml
modules/ssl/mod_ssl.c
modules/ssl/ssl_engine_config.c
modules/ssl/ssl_engine_init.c
modules/ssl/ssl_engine_kernel.c
modules/ssl/ssl_engine_vars.c
modules/ssl/ssl_private.h
test/travis_before_linux.sh

index 449532d5df0d9503c69a59e9245d867d5956afff..dd89c3aebfe40630e7e854b6fe871547cf9156fe 100644 (file)
@@ -316,6 +316,18 @@ jobs:
               APU_CONFIG="--without-crypto"
             pkgs: subversion
           # -------------------------------------------------------------------------
+          - name: OpenSSL ECH branch
+            config: --enable-mods-shared=most --enable-maintainer-mode --disable-md --disable-http2 --disable-ldap --disable-crypto
+            notest-cflags: -Werror -O2
+            env: |
+              TEST_OPENSSL3=ech
+              TEST_OPENSSL3_BRANCH=feature/ech
+              OPENSSL_CONFIG=no-engine
+              APR_VERSION=1.7.6
+              APU_VERSION=1.6.3
+              APU_CONFIG="--without-crypto"
+            pkgs: subversion
+          # -------------------------------------------------------------------------
     runs-on: ${{ matrix.os == '' && 'ubuntu-latest' || matrix.os }}
     timeout-minutes: 30
     env:
diff --git a/changes-entries/ech.txt b/changes-entries/ech.txt
new file mode 100644 (file)
index 0000000..3d3ce48
--- /dev/null
@@ -0,0 +1,2 @@
+  *) mod_ssl: Add support for Encrypted Client Hello (ECH)
+     Github #551.  [Stephen Farrell <stephen.farrell cs.tcd.ie>]
index 9a5b59edd03c9d4cf6840c3c0cd89518d205aac5..772a41124dac8b913f586e7fd169889cd602f259 100644 (file)
@@ -1 +1 @@
-10519
+10542
index fe92af5bb719eb3e0e4cfb2b729abf83b5943a56..854a84dc73a4fa584012a21a0d2c8d0acb406eeb 100644 (file)
@@ -118,6 +118,9 @@ compatibility variables.</p>
 <tr><td><code>SSL_CLIENTHELLO_SIG_ALGOS</code></td>     <td>string</td>    <td>Value of Signature Algorithms extension (13) from ClientHello as four hex encoded characters per item</td></tr>
 <tr><td><code>SSL_CLIENTHELLO_ALPN</code></td>          <td>string</td>    <td>Value of ALPN extension (16) from ClientHello as hex encoded string including leading string lengths</td></tr>
 <tr><td><code>SSL_CLIENTHELLO_VERSIONS</code></td>      <td>string</td>    <td>Value of Supported Versions extension (43) from ClientHello as four hex encoded characters per item</td></tr>
+<tr><td><code>SSL_ECH_STATUS</code></td>                <td>string</td>    <td><code>success</code> means that others also mean what they say</td></tr>
+<tr><td><code>SSL_ECH_INNER_SNI</code></td>             <td>string</td>    <td>SNI value that was encrypted in ECH (or `NONE`)</td></tr>
+<tr><td><code>SSL_ECH_OUTER_SNI</code></td>             <td>string</td>    <td>SNI value that was seen in plaintext SNI (or `NONE`)</td></tr>
 </table>
 
 <p><em>x509</em> specifies a component of an X.509 DN; one of
@@ -3016,4 +3019,123 @@ httpd -t -D DUMP_SSL_POLICIES
 </usage>
 </directivesynopsis>
 
+<directivesynopsis>
+<name>SSLECHKeyDir</name>
+<description>Load the set of Encrypted Client Hello (ECH) PEM files in the named directory</description>
+<syntax>SSLECHKeyDir <em>dirname</em></syntax>
+<contextlist><context>server config</context></contextlist>
+<compatibility>Available in Apache HTTP Server 2.5.1 and later</compatibility>
+
+<usage>
+
+<p>
+ECH is specified in
+    <a href="https://datatracker.ietf.org/doc/draft-ietf-tls-esni/">draft-ietf-tls-esni</a>
+httpd supports ECH "shared-mode" where the httpd instance does the
+ECH decryption and also hosts both the ECH `public-name` and `backend` web
+sites.  
+</p>
+
+<p>
+The <code>SSLECHKeyDir</code> directive 
+names the directory where ECH PEM files (named <code>*.ech</code>) are stored.
+Once an ECH PEM file is successfully loaded, httpd will perform ECH decryption
+and, if that succeeds, will process the relevant TLS session using the 
+SNI from the inner ClientHello.
+</p>
+
+<example><title>Example ECH Config</title>
+<highlight language="config">
+...
+SSLEngine On
+SSLProtocol TLSv1.3
+SSLECHKeyDir /etc/apache2/echkeydir
+...
+# virtual hosts
+&lt;VirtualHost *:443&gt;
+    SSLEngine On
+    SSLProtocol TLSv1.3
+    ServerName example.com
+    DocumentRoot "/var/www/dir-example.com"
+&lt;/VirtualHost&gt;
+&lt;VirtualHost *:443&gt;
+    SSLEngine On
+    SSLProtocol TLSv1.3
+    ServerName foo.example.com
+    DocumentRoot "/var/www/dir-foo.example.com"
+&lt;/VirtualHost&gt;
+...
+</highlight>
+</example>
+
+<note><title>ECH Key Generation and Publication</title>
+<p>
+In the above, we describe a configuration that uses <code>example.com</code> as the
+ECH <code>public-name</code> and where <code>foo.example.com</code> is a web-site for which we want
+ECH to be used, with both hosted on the same httpd instance.
+</p>
+<p>
+Using ECH requries that httpd 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 an ECH PEM
+formatted file as shown below.
+</p>
+<p>
+To generate ECH PEM files, use the ECH-enabled openssl command line
+to generate an ECH key pair and store the result in an ECH PEM file.
+You must also supply the <code>public-name</code> required by the ECH protocol.
+</p>
+<p>
+Key generation operations should be carried out under whatever local account is
+used for httpd configuration.
+</p>
+<example><title>Example: ECH Key Generation</title>
+<highlight language="config">
+~# OSSL=/home/user/code/openssl/apps/openssl
+~# mkdir -p /etc/apache2/echkeydir
+~# chmod 700 /etc/apache2/echkeydir
+~# cd /etc/apache2/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-----
+</highlight>
+</example>
+<p>
+The ECHConfig value then needs to be published in an HTTPS resource record in
+the DNS, so as to be accessible as shown below:
+</p>
+<example><title>Accessing an ECH config from DNS</title>
+<highlight language="config">
+$ dig +short HTTPS foo.example.com
+1 . ech=AD7+DQA6QwAgACA8mxkEsSTp2xXC/RUFCC6CZMMgdM4x1iTWKu3EONjbMAAEAAEAAQALZXhhbXBsZS5vcmcAAA==
+</highlight>
+</example>
+<p>
+Various other fields may be included in an HTTPS resource record. For many
+httpd deployments, existing methods for publishing DNS records may be used to
+achieve the above.  In some cases, one might use
+<a href="https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech">
+A well-known URI for publishing service parameters</a>
+designed to assist web servers in handling e.g. frequent ECH key rotation.
+</p>
+</note>
+
+<note><title>Reloading ECH Keys</title>
+
+<p>
+Giving httpd a command line argument of <code>-k graceful</code> causes a graceful reload
+of the configuration, without dropping existing connections.
+</p>
+
+</note>
+
+</usage>
+</directivesynopsis>
+
 </modulesynopsis>
index e48efb1a476a4faa44ebe22e87eae5ab8400d965..7ccc06006c63f22bb1516f7fd8a42912b1b95aad 100644 (file)
@@ -121,6 +121,11 @@ static const command_rec ssl_config_cmds[] = {
     SSL_CMD_SRV(SessionTicketKeyFile, TAKE1,
                 "TLS session ticket encryption/decryption key file (RFC 5077) "
                 "('/path/to/file' - file with 48 bytes of random data)")
+#endif
+#ifdef HAVE_OPENSSL_ECH
+    SSL_CMD_SRV(ECHKeyDir, TAKE1,
+                "TLS ECH Key Directory"
+                "('/path/to/dir' - directory with ECH key pairs)")
 #endif
     SSL_CMD_ALL(CACertificatePath, TAKE1,
                 "SSL CA Certificate path "
index 50c61bca32013ece1d1c8db85f287ca2405cac5e..e9c65e9995545963ed7e6c7ebf6a26381abf1eab 100644 (file)
@@ -222,6 +222,9 @@ static SSLSrvConfigRec *ssl_config_server_new(apr_pool_t *p)
 #endif
     sc->clienthello_vars       = UNSET;
     sc->session_tickets        = UNSET;
+#ifdef HAVE_OPENSSL_ECH
+    sc->echkeydir              = NULL;
+#endif
 
     modssl_ctx_init_server(sc, p);
 
@@ -356,6 +359,9 @@ void *ssl_config_server_merge(apr_pool_t *p, void *basev, void *addv)
     cfgMergeBool(compression);
 #endif
     cfgMergeBool(session_tickets);
+#ifdef HAVE_OPENSSL_ECH
+    cfgMergeString(echkeydir);
+#endif
 
     modssl_ctx_cfg_merge_server(p, base->server, add->server, mrg->server);
 
@@ -840,6 +846,25 @@ const char *ssl_cmd_SSLEngine(cmd_parms *cmd, void *dcfg, const char *arg)
     return "Argument must be On or Off";
 }
 
+#ifdef HAVE_OPENSSL_ECH
+const char *ssl_cmd_SSLECHKeyDir(cmd_parms *cmd, void *dcfg, const char *arg)
+{
+    SSLSrvConfigRec *sc = mySrvConfig(cmd->server);
+
+    sc->echkeydir=arg;
+
+#if !defined(SSL_HAVE_PROTOCOL_TLSV1_3)
+    ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10533)
+                 "ECHKeyDir configured but TLSv1.3 not supported - exiting.");
+    return "ECHKeyDir configured but TLSv1.3 not supported";
+#endif
+    ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, cmd->server,
+                 "%s: ECHKeyDir set to %s",
+                 cmd->cmd->name, sc->echkeydir);
+    return NULL;
+}
+#endif
+
 const char *ssl_cmd_SSLFIPS(cmd_parms *cmd, void *dcfg, int flag)
 {
 #ifdef HAVE_FIPS
@@ -2654,6 +2679,9 @@ static void ssl_srv_dump(SSLSrvConfigRec *sc, apr_pool_t *p,
     DMP_LONG(  "SSLSessionCacheTimeout", sc->session_cache_timeout);
     DMP_ON_OFF("SSLStrictSNIVHostCheck", sc->strict_sni_vhost_check);
     DMP_ON_OFF("SSLSessionTickets", sc->session_tickets);
+#ifdef HAVE_OPENSSL_ECH
+    DMP_STRING("SSLECHKeyDir", sc->echkeydir);
+#endif
 }
 
 static void ssl_policy_dump(SSLSrvConfigRec *policy, apr_pool_t *p, 
index bdbe594215d5afd4c22b320666cbefa123fcc75f..c47684a3da35bbff0030e0777586486540849118 100644 (file)
@@ -189,6 +189,114 @@ static void ssl_add_version_components(apr_pool_t *ptemp, apr_pool_t *pconf,
                  modver, AP_SERVER_BASEVERSION, incver);
 }
 
+#ifdef HAVE_OPENSSL_ECH
+/*
+ * Load any ECH PEM files we find in the ECHKeyDir directory
+ * Those are files matching "*.ech"
+ * The caller checks that echdir is non-NULL.
+ */
+static int load_echkeys(SSL_CTX *ctx, const char *echdir, server_rec *s,
+                        apr_pool_t *ptemp)
+{
+    size_t elen = 0;
+    int keystried = 0, keysworked = 0, keysloaded=0;
+    OSSL_ECHSTORE *es = NULL;
+    apr_dir_t *dir = NULL;
+    apr_finfo_t direntry;
+    apr_int32_t finfo_flags = APR_FINFO_TYPE|APR_FINFO_NAME;
+    apr_status_t dorv;
+
+    elen = strlen(echdir);
+    if ((elen + 7) >= PATH_MAX) {
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10521)
+                     "load_echkeys: directory name too long: %s - exiting", echdir);
+        return -1;
+    }
+    dorv = apr_dir_open(&dir, echdir, ptemp);
+    if (dorv != APR_SUCCESS) {
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10522)
+                     "load_echkeys: can't open directory %s - exiting (error %d)",
+                     echdir, dorv);
+        return -1;
+    }
+    es = OSSL_ECHSTORE_new(NULL, NULL);
+    if (es == NULL) {
+        apr_dir_close(dir);
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10523)
+                "load_echkeys: can't alloc store");
+        return -1;
+    }
+
+    while ((apr_dir_read(&direntry, finfo_flags, dir)) == APR_SUCCESS) {
+        const char *fname;
+        size_t pnlen = 0;
+        apr_finfo_t theinfo;
+
+        if (direntry.filetype == APR_DIR) {
+            continue; /* don't try to load directories */
+        }
+        fname = apr_pstrcat(ptemp, echdir, "/", direntry.name, NULL);
+        if (!fname) {
+            continue;
+        }
+        pnlen = strlen(fname);
+        if (pnlen < 5 || pnlen > (PATH_MAX-1)) {
+            continue;
+        }
+        if (!(fname[pnlen - 4] == '.'
+            && fname[pnlen - 3] == 'e'
+            && fname[pnlen - 2] == 'c'
+            && fname[pnlen - 1] == 'h')) {
+            continue;
+        }
+        if (apr_stat(&theinfo, fname, APR_FINFO_MIN, ptemp) == APR_SUCCESS) {
+            BIO *in = BIO_new_file(fname, "r");
+            const int is_retry_config = OSSL_ECH_FOR_RETRY;
+
+            keystried++;
+            if (in && OSSL_ECHSTORE_read_pem(es, in, is_retry_config) == 1) {
+                ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, s,
+                             "load_echkeys: worked for %s",fname);
+                keysworked++;
+            }
+            else {
+                ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10525)
+                             "load_echkeys: failed for %s (could be non-fatal)",
+                             fname);
+            }
+            BIO_free_all(in);
+        }
+
+    }
+    apr_dir_close(dir);
+
+    if (!OSSL_ECHSTORE_num_keys(es, &keysloaded)) {
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10526)
+            "OSSL_ECHSTORE_num_keys failed - exiting");
+        OSSL_ECHSTORE_free(es);
+        return -1;
+    }
+    if (SSL_CTX_set1_echstore(ctx, es) != 1) {
+        ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10527)
+            "load_echkeys: SSL_CTX_set1_echstore failed");
+        OSSL_ECHSTORE_free(es);
+        return -1;
+    }
+    OSSL_ECHSTORE_free(es);
+    if (keysworked == 0) {
+        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10528)
+                     "load_echkeys: didn't load new keys (%d tried/failed) "
+                     "but we have already some (%d) - continuing",
+                     keystried, keysloaded);
+    }
+    else {
+        ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10529)
+                     "ECH: %d keys loaded", keysloaded);
+    }
+    return 0;
+}
+#endif
+
 /*  _________________________________________________________________
 **
 **  Let other answer special connection attempts. 
@@ -545,6 +653,9 @@ static apr_status_t ssl_init_ctx_tls_extensions(server_rec *s,
                                                 modssl_ctx_t *mctx)
 {
     apr_status_t rv;
+#ifdef HAVE_OPENSSL_ECH
+    SSLSrvConfigRec *sc = mySrvConfig(s);
+#endif
 
     /*
      * Configure TLS extensions support
@@ -577,6 +688,13 @@ static apr_status_t ssl_init_ctx_tls_extensions(server_rec *s,
     SSL_CTX_set_client_hello_cb(mctx->ssl_ctx, ssl_callback_ClientHello, NULL);
 #endif
 
+#ifdef HAVE_OPENSSL_ECH
+    if (sc != NULL && sc->echkeydir != NULL) {
+        /* callback logs ECH outcome */
+        SSL_CTX_ech_set_callback(mctx->ssl_ctx, ssl_callback_ECH);
+    } 
+#endif
+
 #ifdef HAVE_OCSP_STAPLING
     /*
      * OCSP Stapling support, status_request extension
@@ -903,7 +1021,30 @@ static apr_status_t ssl_init_ctx_protocol(server_rec *s,
         SSL_CTX_set_options(ctx, SSL_OP_IGNORE_UNEXPECTED_EOF);
     }
 #endif
-    
+
+#ifdef HAVE_OPENSSL_ECH
+    /* ECH only really makes sense for TLSv1.3 */
+    prot = SSL_CTX_get_max_proto_version(ctx);
+    if (sc->echkeydir) {
+        if (prot == TLS1_3_VERSION) {
+            /* try load the keys */
+            if (load_echkeys(ctx, sc->echkeydir, s, ptemp) != 0) {
+                ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10531)
+                    "ECHKeyDir failed to load keys - exiting.");
+                SSL_CTX_free(ctx);
+                mctx->ssl_ctx = NULL;
+                return ssl_die(s);
+            }
+        } else {
+            ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10532)
+                 "ECHKeyDir configured but TLSv1.3 turned off - exiting.");
+            SSL_CTX_free(ctx);
+            mctx->ssl_ctx = NULL;
+            return ssl_die(s);
+        } 
+    }
+#endif
+
     return APR_SUCCESS;
 }
 
index 45d986364f79da0b66704357ea76134e4fc01a35..aa9c76ec04ec67d63d1b968528fca63c2ccef1af 100644 (file)
 #include "util_md5.h"
 #include "scoreboard.h"
 
+#ifdef HAVE_OPENSSL_ECH
+#include <openssl/ech.h>
+#endif
+
 static void ssl_configure_env(request_rec *r, SSLConnRec *sslconn);
 #ifdef HAVE_TLSEXT
 static int ssl_find_vhost(void *servername, conn_rec *c, server_rec *s);
@@ -1516,6 +1520,11 @@ int ssl_hook_Fixup(request_rec *r)
         apr_table_set(env, "SSL_TLS_SNI", servername);
     }
 #endif
+#ifdef HAVE_OPENSSL_ECH
+    extract_to_env(r, env, "SSL_ECH_INNER_SNI");
+    extract_to_env(r, env, "SSL_ECH_OUTER_SNI");
+    extract_to_env(r, env, "SSL_ECH_STATUS");
+#endif
 
     /* standard SSL environment variables */
     if (dc->nOptions & SSL_OPT_STDENVVARS) {
@@ -2377,6 +2386,46 @@ static apr_status_t init_vhost(conn_rec *c, SSL *ssl, const char *servername)
     return APR_NOTFOUND;
 }
 
+#ifdef HAVE_OPENSSL_ECH
+unsigned int ssl_callback_ECH(SSL *ssl, const char *str)  
+{
+    char *inner_sni = NULL, *outer_sni = NULL;
+    int echrv;
+    conn_rec *c = NULL;
+    const char *ech_servername;
+
+    c = (conn_rec *)SSL_get_app_data(ssl);
+    ech_servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+    if (ech_servername == NULL) {
+        return SSL_TLSEXT_ERR_NOACK;
+    }
+    echrv = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni);
+    switch (echrv) {
+    case SSL_ECH_STATUS_NOT_TRIED:
+        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "ECH not attempted");
+        break;
+    case SSL_ECH_STATUS_FAILED:
+        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "ECH tried but failed");
+        break;
+    case SSL_ECH_STATUS_BAD_NAME:
+        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, "ECH worked but bad name");
+        break;
+    case SSL_ECH_STATUS_SUCCESS:
+        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, 
+                      "ECH success outer_sni: %s inner_sni: %s",
+                      (outer_sni ? outer_sni : "NONE"),
+                      (inner_sni ? inner_sni : "NONE"));
+        break;
+    default:
+        ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c,
+                      "Error getting ECH status");
+    }
+    OPENSSL_free(inner_sni);
+    OPENSSL_free(outer_sni);
+    return 1;
+}
+#endif
+
 /*
  * This callback function is executed when OpenSSL encounters an extended
  * client hello with a server name indication extension ("SNI", cf. RFC 6066).
index 45a5a5bab6f657ac7826c1653fea332607b66e87..34989f1d247741f4545f031f329b7ed51875f1ea 100644 (file)
@@ -430,6 +430,50 @@ const char *ssl_var_lookup(apr_pool_t *p, server_rec *s,
     return result ? result : "";
 }
 
+#ifdef HAVE_OPENSSL_ECH
+/* Extract ECH status variable from SSL object 'ssl' */
+static const char *ssl_var_lookup_ech_status(apr_pool_t *p, const char *var,
+                                             SSL *ssl)
+{
+    char *inner_sni = NULL, *outer_sni = NULL;
+    int echrv;
+    const char *result = NULL;
+
+    if (ssl == NULL)
+        return result;
+    echrv = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni);
+    if (strcEQ(var, "STATUS")) {
+        switch (echrv) {
+        case SSL_ECH_STATUS_NOT_TRIED:
+            result = "not attempted";
+            break;
+        case SSL_ECH_STATUS_FAILED:
+            result = "tried but failed";
+            break;
+        case SSL_ECH_STATUS_BAD_NAME:
+            result = "ECH worked but bad name";
+            break;
+        case SSL_ECH_STATUS_SUCCESS:
+            result = "success";
+            break;
+        default:
+            result = "error getting ECH status";
+        }
+    }
+    else if (echrv == SSL_ECH_STATUS_SUCCESS) {
+        if (strcEQ(var, "INNER_SNI")) {
+            result = apr_pstrdup(p, inner_sni);
+        }
+        if (strcEQ(var, "OUTER_SNI")) {
+            result = apr_pstrdup(p, outer_sni);
+        }
+    }
+    OPENSSL_free(inner_sni);
+    OPENSSL_free(outer_sni);
+    return result;
+}
+#endif
+
 static const char *ssl_var_lookup_ssl(apr_pool_t *p, const SSLConnRec *sslconn,
                                       request_rec *r, const char *var)
 {
@@ -544,6 +588,11 @@ static const char *ssl_var_lookup_ssl(apr_pool_t *p, const SSLConnRec *sslconn,
         }
     }
 #endif
+#ifdef HAVE_OPENSSL_ECH
+    else if (ssl != NULL && strcEQn(var, "ECH_", 4)) {
+        result = ssl_var_lookup_ech_status(p, var+4, ssl);
+    }
+#endif
 
     return result;
 }
index 071bd69b4c885fe24a1bdacc5a9edbb53859df0e..3f64841737ec119c4ff74b88482221b2f9e7f5bc 100644 (file)
 #define MODSSL_HAVE_OPENSSL_STORE 0
 #endif
 
+/*
+ * Check if we have an ECH-enabled OpenSSL. If we do then this symbol will
+ * be defined in ssl.h and we compile in ECH code.
+ */
+#if defined(SSL_OP_ECH_GREASE)
+#define HAVE_OPENSSL_ECH
+#else
+#undef HAVE_OPENSSL_ECH
+#endif
+
 #if (OPENSSL_VERSION_NUMBER < 0x0090801f)
 #error mod_ssl requires OpenSSL 0.9.8a or later
 #endif
@@ -870,6 +880,9 @@ struct SSLSrvConfigRec {
 #endif
     BOOL             session_tickets;
     BOOL             clienthello_vars;
+#ifdef HAVE_OPENSSL_ECH
+    const char      *echkeydir;
+#endif
 };
 
 /**
@@ -917,6 +930,9 @@ const char  *ssl_cmd_SSLPassPhraseDialog(cmd_parms *, void *, const char *);
 const char  *ssl_cmd_SSLCryptoDevice(cmd_parms *, void *, const char *);
 const char  *ssl_cmd_SSLRandomSeed(cmd_parms *, void *, const char *, const char *, const char *);
 const char  *ssl_cmd_SSLEngine(cmd_parms *, void *, const char *);
+#ifdef HAVE_OPENSSL_ECH
+const char  *ssl_cmd_SSLECHKeyDir(cmd_parms *cmd, void *dcfg, const char *arg);
+#endif
 const char  *ssl_cmd_SSLCipherSuite(cmd_parms *, void *, const char *, const char *);
 const char  *ssl_cmd_SSLCertificateFile(cmd_parms *, void *, const char *);
 const char  *ssl_cmd_SSLCertificateKeyFile(cmd_parms *, void *, const char *);
@@ -1031,6 +1047,9 @@ int          ssl_callback_ServerNameIndication(SSL *, int *, modssl_ctx_t *);
 #endif
 #if OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER)
 int          ssl_callback_ClientHello(SSL *, int *, void *);
+#ifdef HAVE_OPENSSL_ECH
+unsigned int ssl_callback_ECH(SSL *, const char *);
+#endif
 #endif
 #ifdef HAVE_TLS_SESSION_TICKETS
 int ssl_callback_SessionTicket(SSL *ssl,
index 9b6267722daca3f0b818e791e6e20242316ba8df..b163e759f0f559f91561085e71659849004b6f0f 100755 (executable)
@@ -172,8 +172,12 @@ if test -v TEST_OPENSSL3; then
 
         mkdir -p build/openssl
         pushd build/openssl
-           curl -L "https://github.com/openssl/openssl/releases/download/openssl-${TEST_OPENSSL3}/openssl-${TEST_OPENSSL3}.tar.gz" |
-              tar -xzf -
+           if test -v TEST_OPENSSL3_BRANCH; then
+               git clone -b $TEST_OPENSSL3_BRANCH -q https://github.com/openssl/openssl openssl-${TEST_OPENSSL3}
+           else
+               curl -L "https://github.com/openssl/openssl/releases/download/openssl-${TEST_OPENSSL3}/openssl-${TEST_OPENSSL3}.tar.gz" |
+                   tar -xzf -
+           fi
            cd openssl-${TEST_OPENSSL3}
            # Build with RPATH so ./bin/openssl doesn't require $LD_LIBRARY_PATH
            ./Configure --prefix=$HOME/root/openssl3 \