Skip to content

Commit 0c9cd09

Browse files
committed
mod_ssl: Add support for Encrypted Client Hello (ECH) based off
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
1 parent 9cd6c92 commit 0c9cd09

File tree

11 files changed

+435
-4
lines changed

11 files changed

+435
-4
lines changed

.github/workflows/linux.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,18 @@ jobs:
316316
APU_CONFIG="--without-crypto"
317317
pkgs: subversion
318318
# -------------------------------------------------------------------------
319+
- name: OpenSSL ECH branch
320+
config: --enable-mods-shared=most --enable-maintainer-mode --disable-md --disable-http2 --disable-ldap --disable-crypto
321+
notest-cflags: -Werror -O2
322+
env: |
323+
TEST_OPENSSL3=ech
324+
TEST_OPENSSL3_BRANCH=feature/ech
325+
OPENSSL_CONFIG=no-engine
326+
APR_VERSION=1.7.6
327+
APU_VERSION=1.6.3
328+
APU_CONFIG="--without-crypto"
329+
pkgs: subversion
330+
# -------------------------------------------------------------------------
319331
runs-on: ${{ matrix.os == '' && 'ubuntu-latest' || matrix.os }}
320332
timeout-minutes: 30
321333
env:

changes-entries/ech.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*) mod_ssl: Add support for Encrypted Client Hello (ECH)
2+
Github #551. [Stephen Farrell <stephen.farrell cs.tcd.ie>]

docs/log-message-tags/next-number

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
10519
1+
10542

docs/manual/mod/mod_ssl.xml

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ compatibility variables.</p>
118118
<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>
119119
<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>
120120
<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>
121+
<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>
122+
<tr><td><code>SSL_ECH_INNER_SNI</code></td> <td>string</td> <td>SNI value that was encrypted in ECH (or `NONE`)</td></tr>
123+
<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>
121124
</table>
122125

123126
<p><em>x509</em> specifies a component of an X.509 DN; one of
@@ -3016,4 +3019,123 @@ httpd -t -D DUMP_SSL_POLICIES
30163019
</usage>
30173020
</directivesynopsis>
30183021

3022+
<directivesynopsis>
3023+
<name>SSLECHKeyDir</name>
3024+
<description>Load the set of Encrypted Client Hello (ECH) PEM files in the named directory</description>
3025+
<syntax>SSLECHKeyDir <em>dirname</em></syntax>
3026+
<contextlist><context>server config</context></contextlist>
3027+
<compatibility>Available in Apache HTTP Server 2.5.1 and later</compatibility>
3028+
3029+
<usage>
3030+
3031+
<p>
3032+
ECH is specified in
3033+
<a href="https://datatracker.ietf.org/doc/draft-ietf-tls-esni/">draft-ietf-tls-esni</a>
3034+
httpd supports ECH "shared-mode" where the httpd instance does the
3035+
ECH decryption and also hosts both the ECH `public-name` and `backend` web
3036+
sites.
3037+
</p>
3038+
3039+
<p>
3040+
The <code>SSLECHKeyDir</code> directive
3041+
names the directory where ECH PEM files (named <code>*.ech</code>) are stored.
3042+
Once an ECH PEM file is successfully loaded, httpd will perform ECH decryption
3043+
and, if that succeeds, will process the relevant TLS session using the
3044+
SNI from the inner ClientHello.
3045+
</p>
3046+
3047+
<example><title>Example ECH Config</title>
3048+
<highlight language="config">
3049+
...
3050+
SSLEngine On
3051+
SSLProtocol TLSv1.3
3052+
SSLECHKeyDir /etc/apache2/echkeydir
3053+
...
3054+
# virtual hosts
3055+
&lt;VirtualHost *:443&gt;
3056+
SSLEngine On
3057+
SSLProtocol TLSv1.3
3058+
ServerName example.com
3059+
DocumentRoot "/var/www/dir-example.com"
3060+
&lt;/VirtualHost&gt;
3061+
&lt;VirtualHost *:443&gt;
3062+
SSLEngine On
3063+
SSLProtocol TLSv1.3
3064+
ServerName foo.example.com
3065+
DocumentRoot "/var/www/dir-foo.example.com"
3066+
&lt;/VirtualHost&gt;
3067+
...
3068+
</highlight>
3069+
</example>
3070+
3071+
<note><title>ECH Key Generation and Publication</title>
3072+
<p>
3073+
In the above, we describe a configuration that uses <code>example.com</code> as the
3074+
ECH <code>public-name</code> and where <code>foo.example.com</code> is a web-site for which we want
3075+
ECH to be used, with both hosted on the same httpd instance.
3076+
</p>
3077+
<p>
3078+
Using ECH requries that httpd load an ECH key pair with a private value for ECH
3079+
decryption. Browsers will require that the public component of that key pair be
3080+
published in the DNS. With OpenSSL we generate and store that key pair in an ECH PEM
3081+
formatted file as shown below.
3082+
</p>
3083+
<p>
3084+
To generate ECH PEM files, use the ECH-enabled openssl command line
3085+
to generate an ECH key pair and store the result in an ECH PEM file.
3086+
You must also supply the <code>public-name</code> required by the ECH protocol.
3087+
</p>
3088+
<p>
3089+
Key generation operations should be carried out under whatever local account is
3090+
used for httpd configuration.
3091+
</p>
3092+
<example><title>Example: ECH Key Generation</title>
3093+
<highlight language="config">
3094+
~# OSSL=/home/user/code/openssl/apps/openssl
3095+
~# mkdir -p /etc/apache2/echkeydir
3096+
~# chmod 700 /etc/apache2/echkeydir
3097+
~# cd /etc/apache2/echkeydir
3098+
~# $OSSL ech -public-name example.com -o example.com.pem.ech
3099+
~# cat example.com.pem.ech
3100+
-----BEGIN PRIVATE KEY-----
3101+
MC4CAQAwBQYDK2VuBCIEIJi22Im2rJ/lJqzNFZdGfsVfmknXAc8xz3fYPhD0Na5I
3102+
-----END PRIVATE KEY-----
3103+
-----BEGIN ECHCONFIG-----
3104+
AD7+DQA6QwAgACA8mxkEsSTp2xXC/RUFCC6CZMMgdM4x1iTWKu3EONjbMAAEAAEA
3105+
AQALZXhhbXBsZS5vcmcAAA==
3106+
-----END ECHCONFIG-----
3107+
</highlight>
3108+
</example>
3109+
<p>
3110+
The ECHConfig value then needs to be published in an HTTPS resource record in
3111+
the DNS, so as to be accessible as shown below:
3112+
</p>
3113+
<example><title>Accessing an ECH config from DNS</title>
3114+
<highlight language="config">
3115+
$ dig +short HTTPS foo.example.com
3116+
1 . ech=AD7+DQA6QwAgACA8mxkEsSTp2xXC/RUFCC6CZMMgdM4x1iTWKu3EONjbMAAEAAEAAQALZXhhbXBsZS5vcmcAAA==
3117+
</highlight>
3118+
</example>
3119+
<p>
3120+
Various other fields may be included in an HTTPS resource record. For many
3121+
httpd deployments, existing methods for publishing DNS records may be used to
3122+
achieve the above. In some cases, one might use
3123+
<a href="https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech">
3124+
A well-known URI for publishing service parameters</a>
3125+
designed to assist web servers in handling e.g. frequent ECH key rotation.
3126+
</p>
3127+
</note>
3128+
3129+
<note><title>Reloading ECH Keys</title>
3130+
3131+
<p>
3132+
Giving httpd a command line argument of <code>-k graceful</code> causes a graceful reload
3133+
of the configuration, without dropping existing connections.
3134+
</p>
3135+
3136+
</note>
3137+
3138+
</usage>
3139+
</directivesynopsis>
3140+
30193141
</modulesynopsis>

modules/ssl/mod_ssl.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ static const command_rec ssl_config_cmds[] = {
121121
SSL_CMD_SRV(SessionTicketKeyFile, TAKE1,
122122
"TLS session ticket encryption/decryption key file (RFC 5077) "
123123
"('/path/to/file' - file with 48 bytes of random data)")
124+
#endif
125+
#ifdef HAVE_OPENSSL_ECH
126+
SSL_CMD_SRV(ECHKeyDir, TAKE1,
127+
"TLS ECH Key Directory"
128+
"('/path/to/dir' - directory with ECH key pairs)")
124129
#endif
125130
SSL_CMD_ALL(CACertificatePath, TAKE1,
126131
"SSL CA Certificate path "

modules/ssl/ssl_engine_config.c

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ static SSLSrvConfigRec *ssl_config_server_new(apr_pool_t *p)
222222
#endif
223223
sc->clienthello_vars = UNSET;
224224
sc->session_tickets = UNSET;
225+
#ifdef HAVE_OPENSSL_ECH
226+
sc->echkeydir = NULL;
227+
#endif
225228

226229
modssl_ctx_init_server(sc, p);
227230

@@ -356,6 +359,9 @@ void *ssl_config_server_merge(apr_pool_t *p, void *basev, void *addv)
356359
cfgMergeBool(compression);
357360
#endif
358361
cfgMergeBool(session_tickets);
362+
#ifdef HAVE_OPENSSL_ECH
363+
cfgMergeString(echkeydir);
364+
#endif
359365

360366
modssl_ctx_cfg_merge_server(p, base->server, add->server, mrg->server);
361367

@@ -840,6 +846,25 @@ const char *ssl_cmd_SSLEngine(cmd_parms *cmd, void *dcfg, const char *arg)
840846
return "Argument must be On or Off";
841847
}
842848

849+
#ifdef HAVE_OPENSSL_ECH
850+
const char *ssl_cmd_SSLECHKeyDir(cmd_parms *cmd, void *dcfg, const char *arg)
851+
{
852+
SSLSrvConfigRec *sc = mySrvConfig(cmd->server);
853+
854+
sc->echkeydir=arg;
855+
856+
#if !defined(SSL_HAVE_PROTOCOL_TLSV1_3)
857+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10533)
858+
"ECHKeyDir configured but TLSv1.3 not supported - exiting.");
859+
return "ECHKeyDir configured but TLSv1.3 not supported";
860+
#endif
861+
ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, cmd->server,
862+
"%s: ECHKeyDir set to %s",
863+
cmd->cmd->name, sc->echkeydir);
864+
return NULL;
865+
}
866+
#endif
867+
843868
const char *ssl_cmd_SSLFIPS(cmd_parms *cmd, void *dcfg, int flag)
844869
{
845870
#ifdef HAVE_FIPS
@@ -2654,6 +2679,9 @@ static void ssl_srv_dump(SSLSrvConfigRec *sc, apr_pool_t *p,
26542679
DMP_LONG( "SSLSessionCacheTimeout", sc->session_cache_timeout);
26552680
DMP_ON_OFF("SSLStrictSNIVHostCheck", sc->strict_sni_vhost_check);
26562681
DMP_ON_OFF("SSLSessionTickets", sc->session_tickets);
2682+
#ifdef HAVE_OPENSSL_ECH
2683+
DMP_STRING("SSLECHKeyDir", sc->echkeydir);
2684+
#endif
26572685
}
26582686

26592687
static void ssl_policy_dump(SSLSrvConfigRec *policy, apr_pool_t *p,

modules/ssl/ssl_engine_init.c

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,114 @@ static void ssl_add_version_components(apr_pool_t *ptemp, apr_pool_t *pconf,
189189
modver, AP_SERVER_BASEVERSION, incver);
190190
}
191191

192+
#ifdef HAVE_OPENSSL_ECH
193+
/*
194+
* Load any ECH PEM files we find in the ECHKeyDir directory
195+
* Those are files matching "*.ech"
196+
* The caller checks that echdir is non-NULL.
197+
*/
198+
static int load_echkeys(SSL_CTX *ctx, const char *echdir, server_rec *s,
199+
apr_pool_t *ptemp)
200+
{
201+
size_t elen = 0;
202+
int keystried = 0, keysworked = 0, keysloaded=0;
203+
OSSL_ECHSTORE *es = NULL;
204+
apr_dir_t *dir = NULL;
205+
apr_finfo_t direntry;
206+
apr_int32_t finfo_flags = APR_FINFO_TYPE|APR_FINFO_NAME;
207+
apr_status_t dorv;
208+
209+
elen = strlen(echdir);
210+
if ((elen + 7) >= PATH_MAX) {
211+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10521)
212+
"load_echkeys: directory name too long: %s - exiting", echdir);
213+
return -1;
214+
}
215+
dorv = apr_dir_open(&dir, echdir, ptemp);
216+
if (dorv != APR_SUCCESS) {
217+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10522)
218+
"load_echkeys: can't open directory %s - exiting (error %d)",
219+
echdir, dorv);
220+
return -1;
221+
}
222+
es = OSSL_ECHSTORE_new(NULL, NULL);
223+
if (es == NULL) {
224+
apr_dir_close(dir);
225+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10523)
226+
"load_echkeys: can't alloc store");
227+
return -1;
228+
}
229+
230+
while ((apr_dir_read(&direntry, finfo_flags, dir)) == APR_SUCCESS) {
231+
const char *fname;
232+
size_t pnlen = 0;
233+
apr_finfo_t theinfo;
234+
235+
if (direntry.filetype == APR_DIR) {
236+
continue; /* don't try to load directories */
237+
}
238+
fname = apr_pstrcat(ptemp, echdir, "/", direntry.name, NULL);
239+
if (!fname) {
240+
continue;
241+
}
242+
pnlen = strlen(fname);
243+
if (pnlen < 5 || pnlen > (PATH_MAX-1)) {
244+
continue;
245+
}
246+
if (!(fname[pnlen - 4] == '.'
247+
&& fname[pnlen - 3] == 'e'
248+
&& fname[pnlen - 2] == 'c'
249+
&& fname[pnlen - 1] == 'h')) {
250+
continue;
251+
}
252+
if (apr_stat(&theinfo, fname, APR_FINFO_MIN, ptemp) == APR_SUCCESS) {
253+
BIO *in = BIO_new_file(fname, "r");
254+
const int is_retry_config = OSSL_ECH_FOR_RETRY;
255+
256+
keystried++;
257+
if (in && OSSL_ECHSTORE_read_pem(es, in, is_retry_config) == 1) {
258+
ap_log_error(APLOG_MARK, APLOG_TRACE4, 0, s,
259+
"load_echkeys: worked for %s",fname);
260+
keysworked++;
261+
}
262+
else {
263+
ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10525)
264+
"load_echkeys: failed for %s (could be non-fatal)",
265+
fname);
266+
}
267+
BIO_free_all(in);
268+
}
269+
270+
}
271+
apr_dir_close(dir);
272+
273+
if (!OSSL_ECHSTORE_num_keys(es, &keysloaded)) {
274+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10526)
275+
"OSSL_ECHSTORE_num_keys failed - exiting");
276+
OSSL_ECHSTORE_free(es);
277+
return -1;
278+
}
279+
if (SSL_CTX_set1_echstore(ctx, es) != 1) {
280+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10527)
281+
"load_echkeys: SSL_CTX_set1_echstore failed");
282+
OSSL_ECHSTORE_free(es);
283+
return -1;
284+
}
285+
OSSL_ECHSTORE_free(es);
286+
if (keysworked == 0) {
287+
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10528)
288+
"load_echkeys: didn't load new keys (%d tried/failed) "
289+
"but we have already some (%d) - continuing",
290+
keystried, keysloaded);
291+
}
292+
else {
293+
ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10529)
294+
"ECH: %d keys loaded", keysloaded);
295+
}
296+
return 0;
297+
}
298+
#endif
299+
192300
/* _________________________________________________________________
193301
**
194302
** Let other answer special connection attempts.
@@ -545,6 +653,9 @@ static apr_status_t ssl_init_ctx_tls_extensions(server_rec *s,
545653
modssl_ctx_t *mctx)
546654
{
547655
apr_status_t rv;
656+
#ifdef HAVE_OPENSSL_ECH
657+
SSLSrvConfigRec *sc = mySrvConfig(s);
658+
#endif
548659

549660
/*
550661
* Configure TLS extensions support
@@ -577,6 +688,13 @@ static apr_status_t ssl_init_ctx_tls_extensions(server_rec *s,
577688
SSL_CTX_set_client_hello_cb(mctx->ssl_ctx, ssl_callback_ClientHello, NULL);
578689
#endif
579690

691+
#ifdef HAVE_OPENSSL_ECH
692+
if (sc != NULL && sc->echkeydir != NULL) {
693+
/* callback logs ECH outcome */
694+
SSL_CTX_ech_set_callback(mctx->ssl_ctx, ssl_callback_ECH);
695+
}
696+
#endif
697+
580698
#ifdef HAVE_OCSP_STAPLING
581699
/*
582700
* OCSP Stapling support, status_request extension
@@ -903,7 +1021,30 @@ static apr_status_t ssl_init_ctx_protocol(server_rec *s,
9031021
SSL_CTX_set_options(ctx, SSL_OP_IGNORE_UNEXPECTED_EOF);
9041022
}
9051023
#endif
906-
1024+
1025+
#ifdef HAVE_OPENSSL_ECH
1026+
/* ECH only really makes sense for TLSv1.3 */
1027+
prot = SSL_CTX_get_max_proto_version(ctx);
1028+
if (sc->echkeydir) {
1029+
if (prot == TLS1_3_VERSION) {
1030+
/* try load the keys */
1031+
if (load_echkeys(ctx, sc->echkeydir, s, ptemp) != 0) {
1032+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10531)
1033+
"ECHKeyDir failed to load keys - exiting.");
1034+
SSL_CTX_free(ctx);
1035+
mctx->ssl_ctx = NULL;
1036+
return ssl_die(s);
1037+
}
1038+
} else {
1039+
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, APLOGNO(10532)
1040+
"ECHKeyDir configured but TLSv1.3 turned off - exiting.");
1041+
SSL_CTX_free(ctx);
1042+
mctx->ssl_ctx = NULL;
1043+
return ssl_die(s);
1044+
}
1045+
}
1046+
#endif
1047+
9071048
return APR_SUCCESS;
9081049
}
9091050

0 commit comments

Comments
 (0)