[PATCH] Tests: added tests for Encrypted Client Hello (ECH)
Maxim Dounin
mdounin at mdounin.ru
Wed Sep 17 01:26:01 UTC 2025
Hello!
On Tue, Sep 09, 2025 at 02:33:08PM +0300, Maxim Dounin wrote:
> # HG changeset patch
> # User Maxim Dounin <mdounin at mdounin.ru>
> # Date 1757416239 -10800
> # Tue Sep 09 14:10:39 2025 +0300
> # Node ID f1f43bdcf99ecadee593ced18f5bb3e2570f31ff
> # Parent 009ff3a25affe30f3db45757366b0bd726f9bf21
> Tests: added tests for Encrypted Client Hello (ECH).
>
> diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t
> new file mode 100644
> --- /dev/null
> +++ b/ssl_encrypted_hello.t
> @@ -0,0 +1,420 @@
> +#!/usr/bin/perl
> +
> +# (C) Maxim Dounin
> +
> +# Tests for http ssl module, support for Encrypted Client Hello (ECH).
> +
> +###############################################################################
> +
> +use warnings;
> +use strict;
> +
> +use Test::More;
> +
> +use MIME::Base64;
> +
> +BEGIN { use FindBin; chdir($FindBin::Bin); }
> +
> +use lib 'lib';
> +use Test::Nginx;
> +
> +###############################################################################
> +
> +select STDERR; $| = 1;
> +select STDOUT; $| = 1;
> +
> +my $t = Test::Nginx->new()
> + ->has(qw/http http_ssl sni rewrite/)
> + ->has_daemon('openssl');
> +
> +$t->write_file_expand('nginx.conf', <<'EOF');
> +
> +%%TEST_GLOBALS%%
> +
> +daemon off;
> +
> +events {
> +}
> +
> +http {
> + %%TEST_GLOBALS_HTTP%%
> +
> + server {
> + listen 127.0.0.1:8443 ssl;
> + server_name public;
> +
> + ssl_certificate public.crt;
> + ssl_certificate_key public.key;
> +
> + ssl_encrypted_hello_key public.ech;
> +
> + return 200 "$ssl_server_name:$ssl_encrypted_hello\n";
> + }
> +
> + server {
> + listen 127.0.0.1:8443 ssl;
> + server_name secret;
> +
> + ssl_certificate secret.crt;
> + ssl_certificate_key secret.key;
> +
> + return 200 "$ssl_server_name:$ssl_encrypted_hello\n";
> + }
> +
> + server {
> + listen 127.0.0.1:8443 ssl;
> + server_name verify;
> +
> + ssl_certificate verify.crt;
> + ssl_certificate_key verify.key;
> +
> + ssl_verify_client optional_no_ca;
> + ssl_client_certificate verify.crt;
> +
> + return 200 "$ssl_server_name:$ssl_encrypted_hello:$ssl_client_verify\n";
> + }
> +}
> +
> +EOF
> +
> +$t->write_file('openssl.conf', <<EOF);
> +[ req ]
> +default_bits = 2048
> +encrypt_key = no
> +distinguished_name = req_distinguished_name
> +[ req_distinguished_name ]
> +EOF
> +
> +my $d = $t->testdir();
> +
> +foreach my $name ('public', 'secret', 'verify') {
> + system('openssl req -x509 -new '
> + . "-config $d/openssl.conf -subj /CN=$name/ "
> + . "-out $d/$name.crt -keyout $d/$name.key "
> + . ">>$d/openssl.out 2>&1") == 0
> + or die "Can't create certificate for $name: $!\n";
> +}
> +
> +$t->write_file(
> + 'trusted.crt',
> + $t->read_file('public.crt')
> + . $t->read_file('secret.crt')
> + . $t->read_file('verify.crt')
> +);
> +
> +if ((`openssl ech -help 2>&1` || '') =~ m/-public_name/) {
> +
> + # Generate ECH file with "openssl ech"
> +
> + system('openssl ech '
> + . "-out $d/public.ech "
> + . "-public_name public "
> + . ">>$d/openssl.out 2>&1") == 0
> + or die "Can't create ECH config: $!\n";
> +
> +} elsif ((`bssl 2>&1` || '') =~ m/generate-ech/) {
> +
> + # Generate ECH file with "bssl generate-ech"
> + # and additional manual formatting to produce a PEM file
> +
> + system('bssl generate-ech '
> + . "-out-ech-config $d/public.echconfig.bin "
> + . "-out-ech-config-list $d/public.echconfiglist.bin "
> + . "-out-private-key $d/public.echkey.bin "
> + . "-public-name public "
> + . "-config-id 0 "
> + . ">>$d/openssl.out 2>&1") == 0
> + or die "Can't create ECH config: $!\n";
> +
> + my $list = $t->read_file('public.echconfiglist.bin');
> + my $key = $t->read_file('public.echkey.bin');
> +
> + # BoringSSL uses raw X25519 private key. Convert it to PKCS#8
> + # PrivateKeyInfo.
> +
> + $key = "\x30\x2E" # SEQUENCE, 46 bytes
> + . "\x02\x01\x00" # INTEGER, 1 byte, 0
> + . "\x30\x05" # SEQUENCE, 5 bytes
> + . "\x06\x03\x2B\x65\x6E" # OBJECT, 3 bytes, X25519
> + . "\x04\x22" # OCTET STRING, 34 bytes
> + . "\x04\x20" # OCTET STRING, 32 bytes
> + . $key;
> +
> + $t->write_file(
> + 'public.ech',
> + "-----BEGIN PRIVATE KEY-----\n"
> + . encode_base64($key)
> + . "-----END PRIVATE KEY-----\n"
> + . "-----BEGIN ECHCONFIG-----\n"
> + . encode_base64($list)
> + . "-----END ECHCONFIG-----\n"
> + );
> +
> +} else {
> + plan(skip_all => 'no openssl ech or bssl generate-ech')
> +}
> +
> +$t->try_run('no ssl_encrypted_hello_key')->plan(8);
> +
> +###############################################################################
> +
> +my ($cmd, $req, $out);
> +my $port = port(8443);
> +
> +# ECH file looks like:
> +#
> +# -----BEGIN PRIVATE KEY-----
> +# MC4CAQAwBQYDK2VuBCIEIMhvGkKTR2gchVcurYDocK4v1Y5wac20UZzB3JB0QMVh
> +# -----END PRIVATE KEY-----
> +# -----BEGIN ECHCONFIG-----
> +# AEX+DQBBGwAgACC2q1Z7YDL1X4bahRyJeBZb3bwHPITBUxqFBS2CIfXCGQAEAAEA
> +# AQAScHVibGljLmV4YW1wbGUub3JnAAA=
> +# -----END ECHCONFIG-----
> +#
> +# To use on the client we need ECHCONFIG part, which contains ECHConfigList
> +# structure.
> +
> +my $config = $t->read_file('public.ech');
> +$config =~ s/.*-----BEGIN ECHCONFIG-----(.*)-----END.*/$1/s;
> +$config =~ s/[\n\r\s]//g;
> +
> +# Requests to use
> +
> +$t->write_file('req-secret', "GET / HTTP/1.0\nHost: secret\n\n");
> +$t->write_file('req-verify', "GET / HTTP/1.0\nHost: verify\n\n");
> +
> +SKIP: {
> +skip 'no openssl client ech', 4
> + if `openssl s_client -help 2>&1` !~ /-ech_config_list/;
> +
> +# Tests with OpenSSL s_client from ECH feature branch
> +
> +# Note that OpenSSL s_client prints confusing "ECH: BAD NAME: -102" status
> +# when it is not able to verify server certificate. To make sure proper
> +# success is visible in the output, we therefore explicitly provide trusted
> +# root certificates.
> +
> +$cmd = "openssl s_client "
> + . "-connect 127.0.0.1:$port "
> + . "-servername secret "
> + . "-ech_config_list $config "
> + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +# Note that OpenSSL s_client from ECH feature branch currently cannot talk
> +# to a server with BoringSSL. BoringSSL error on the server is as follows:
> +#
> +# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines:
> +# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines:
> +# OPENSSL_internal:DECRYPTION_FAILED)...
> +
> +TODO: {
> +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL'
> + if $t->has_module('BoringSSL');
> +local $TODO = 'OpenSSL too old'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && !$t->has_feature('openssl:3.6.0');
> +local $TODO = 'LibreSSL has no support yet'
> + if $t->has_module('LibreSSL');
> +
> +like($out, qr/^ECH: success.*secret:1$/ms, 'openssl client');
> +
> +}
> +
> +# Test without ECH, to make sure the $ssl_encrypted_hello variable
> +# is properly set.
> +#
> +# The test explicitly requests @SECLEVEL=0 for libraries without TLSv1.2
> +# support, such as OpenSSL 1.0.0.
> +
> +$cmd = "openssl s_client "
> + . "-connect 127.0.0.1:$port "
> + . "-servername secret "
> + . "-cipher DEFAULT:\@SECLEVEL=0 "
> + . "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/^ECH: NOT CONFIGURED.*secret:$/ms, 'openssl client no ech');
> +
> +# Tests with client certificate verification,
> +# mostly to check if the $ssl_encrypted_hello variable is correct, notably
> +# with failed client certificate verification.
> +#
> +# Currently fails with OpenSSL ECH feature branch on server, the error is
> +# as follows:
> +#
> +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines::
> +# missing fatal)...
> +#
> +# Also, similarly to the above, this fails with BoringSSL on the server.
> +
> +TODO: {
> +local $TODO = 'OpenSSL broken verify'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && $t->has_feature('openssl:3.6.0');
> +local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL'
> + if $t->has_module('BoringSSL');
> +local $TODO = 'OpenSSL too old'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && !$t->has_feature('openssl:3.6.0');
> +local $TODO = 'LibreSSL has no support yet'
> + if $t->has_module('LibreSSL');
> +
> +$cmd = "openssl s_client "
> + . "-connect 127.0.0.1:$port "
> + . "-servername verify "
> + . "-ech_config_list $config "
> + . "-cert $d/verify.crt "
> + . "-key $d/verify.key "
> + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/^ECH: success.*verify:1:SUCCESS/ms, 'openssl client verify');
> +
> +$cmd = "openssl s_client "
> + . "-connect 127.0.0.1:$port "
> + . "-servername verify "
> + . "-ech_config_list $config "
> + . "-cert $d/secret.crt "
> + . "-key $d/secret.key "
> + . "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/^ECH: success.*verify:1:FAILED/ms,
> + 'openssl client verify failed');
> +
> +}
> +}
> +
> +SKIP: {
> +skip 'no bssl client ech', 4
> + if (`bssl client -help 2>&1` || '') !~ /-ech-config-list/;
> +
> +# Tests with BoringSSL bssl tool
> +
> +# BoringSSL bssl tool uses a file with binary ECHConfigList
> +# representation.
> +
> +$t->write_file('public.bin', decode_base64($config));
> +
> +$cmd = "bssl client "
> + . "-connect 127.0.0.1:$port "
> + . "-server-name secret "
> + . "-ech-config-list $d/public.bin "
> + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +TODO: {
> +local $TODO = 'OpenSSL too old'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && !$t->has_feature('openssl:3.6.0');
> +local $TODO = 'LibreSSL has no support yet'
> + if $t->has_module('LibreSSL');
> +
> +like($out, qr/Encrypted ClientHello: yes.*secret:1$/ms, 'bssl client');
> +
> +}
> +
> +# Test without ECH, to make sure the $ssl_encrypted_hello variable
> +# is properly set.
> +#
> +# The test explicitly requests TLSv1.0 for libraries without TLSv1.2
> +# support, such as OpenSSL 1.0.0.
> +
> +$cmd = "bssl client "
> + . "-connect 127.0.0.1:$port "
> + . "-server-name secret "
> + . "-min-version tls1 "
> + . "-root-certs $d/trusted.crt <$d/req-secret 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/Encrypted ClientHello: no.*secret:$/ms, 'bssl client no ech');
> +
> +# Tests with client certificate verification,
> +# mostly to check if the $ssl_encrypted_hello variable is correct, notably
> +# with failed client certificate verification.
> +#
> +# Currently fails with OpenSSL ECH feature branch on server, the error is
> +# as follows:
> +#
> +# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines::
> +# missing fatal)...
> +
> +TODO: {
> +local $TODO = 'OpenSSL broken verify'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && $t->has_feature('openssl:3.6.0');
> +local $TODO = 'OpenSSL too old'
> + if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
> + && !$t->has_feature('openssl:3.6.0');
> +local $TODO = 'LibreSSL has no support yet'
> + if $t->has_module('LibreSSL');
> +
> +$cmd = "bssl client "
> + . "-connect 127.0.0.1:$port "
> + . "-server-name verify "
> + . "-ech-config-list $d/public.bin "
> + . "-cert $d/verify.crt "
> + . "-key $d/verify.key "
> + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/Encrypted ClientHello: yes.*verify:1:SUCCESS/ms,
> + 'bssl client verify');
> +
> +$cmd = "bssl client "
> + . "-connect 127.0.0.1:$port "
> + . "-server-name verify "
> + . "-ech-config-list $d/public.bin "
> + . "-cert $d/secret.crt "
> + . "-key $d/secret.key "
> + . "-root-certs $d/trusted.crt <$d/req-verify 2>&1";
> +
> +log_out($cmd);
> +
> +$out = `$cmd`;
> +
> +log_in($out);
> +
> +like($out, qr/Encrypted ClientHello: yes.*verify:1:FAILED/ms,
> + 'bssl client verify failed');
> +
> +}
> +}
> +
> +###############################################################################
>
And here are updates to the tests, following feedback from Stephen
Farrell on the observed issues with OpenSSL ECH feature branch:
diff --git a/ssl_encrypted_hello.t b/ssl_encrypted_hello.t
--- a/ssl_encrypted_hello.t
+++ b/ssl_encrypted_hello.t
@@ -193,11 +193,22 @@ skip 'no openssl client ech', 4
# when it is not able to verify server certificate. To make sure proper
# success is visible in the output, we therefore explicitly provide trusted
# root certificates.
+#
+# Further, with TLSv1.2 and older protocols enabled OpenSSL s_client currently
+# creates incorrect inner ClientHello, which is rejected by BoringSSL with
+# the following error on the server:
+#
+# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines:
+# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines:
+# OPENSSL_internal:DECRYPTION_FAILED)...
+#
+# As a workaround, we explicitly request TLSv1.3 only.
$cmd = "openssl s_client "
. "-connect 127.0.0.1:$port "
. "-servername secret "
. "-ech_config_list $config "
+ . "-tls1_3 "
. "-CAfile $d/trusted.crt -ign_eof <$d/req-secret 2>&1";
log_out($cmd);
@@ -206,16 +217,7 @@ log_out($cmd);
log_in($out);
-# Note that OpenSSL s_client from ECH feature branch currently cannot talk
-# to a server with BoringSSL. BoringSSL error on the server is as follows:
-#
-# ... [crit] ... SSL_do_handshake() failed (SSL: error:1000013a:SSL routines:
-# OPENSSL_internal:INVALID_CLIENT_HELLO_INNER error:1000008a:SSL routines:
-# OPENSSL_internal:DECRYPTION_FAILED)...
-
TODO: {
-local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL'
- if $t->has_module('BoringSSL');
local $TODO = 'OpenSSL too old'
if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
&& !$t->has_feature('openssl:3.6.0');
@@ -250,20 +252,19 @@ like($out, qr/^ECH: NOT CONFIGURED.*secr
# mostly to check if the $ssl_encrypted_hello variable is correct, notably
# with failed client certificate verification.
#
-# Currently fails with OpenSSL ECH feature branch on server, the error is
-# as follows:
+# Currently fails with OpenSSL ECH feature branch on the server,
+# the error is as follows:
#
# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines::
# missing fatal)...
#
-# Also, similarly to the above, this fails with BoringSSL on the server.
+# This is expected to be fixed by
+# https://github.com/openssl/openssl/pull/28555.
TODO: {
local $TODO = 'OpenSSL broken verify'
if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
&& $t->has_feature('openssl:3.6.0');
-local $TODO = 'OpenSSL s_client cannot use ECH to BoringSSL'
- if $t->has_module('BoringSSL');
local $TODO = 'OpenSSL too old'
if $t->has_module('OpenSSL') && !$t->has_module('BoringSSL')
&& !$t->has_feature('openssl:3.6.0');
@@ -276,6 +277,7 @@ local $TODO = 'LibreSSL has no support y
. "-ech_config_list $config "
. "-cert $d/verify.crt "
. "-key $d/verify.key "
+ . "-tls1_3 "
. "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1";
log_out($cmd);
@@ -292,6 +294,7 @@ like($out, qr/^ECH: success.*verify:1:SU
. "-ech_config_list $config "
. "-cert $d/secret.crt "
. "-key $d/secret.key "
+ . "-tls1_3 "
. "-CAfile $d/trusted.crt -ign_eof <$d/req-verify 2>&1";
log_out($cmd);
@@ -363,12 +366,6 @@ like($out, qr/Encrypted ClientHello: no.
# Tests with client certificate verification,
# mostly to check if the $ssl_encrypted_hello variable is correct, notably
# with failed client certificate verification.
-#
-# Currently fails with OpenSSL ECH feature branch on server, the error is
-# as follows:
-#
-# ... [crit] ... SSL_do_handshake() failed (SSL: error:0A000100:SSL routines::
-# missing fatal)...
TODO: {
local $TODO = 'OpenSSL broken verify'
--
Maxim Dounin
http://mdounin.ru/
More information about the nginx-devel
mailing list