Commit 5e5a76fc2c for openssl.org
commit 5e5a76fc2c08c1db7036076512acf8c404e9f1f4
Author: sftcd <stephen.farrell@cs.tcd.ie>
Date: Sun Nov 23 23:19:16 2025 +0000
Add tests and documentation and fix a couple of issues identified by added tests
Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.org>
MergeDate: Mon Feb 16 15:41:15 2026
(Merged from https://github.com/openssl/openssl/pull/29200)
diff --git a/.gitignore b/.gitignore
index 750d53b192..25ce225689 100644
--- a/.gitignore
+++ b/.gitignore
@@ -425,3 +425,11 @@ doc-nits
# LSP (Language Server Protocol) support
.cache/
compile_commands.json
+
+# coverage files
+*.gcda
+*.gcno
+lcov.info
+run_tests
+depend
+
diff --git a/INSTALL.md b/INSTALL.md
index 7330a259b1..15964afe1b 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -771,8 +771,7 @@ from ASN.1 formats.
### no-ech
-Don't build support for Encrypted Client Hello (ECH) extension (draft-ietf-tls-esni)
-TODO(ECH) update link to RFC.
+Don't build support for Encrypted Client Hello (ECH) extension.
### enable-ec_nistp_64_gcc_128
diff --git a/apps/ech.c b/apps/ech.c
index f2d3aacde2..74579d82d1 100644
--- a/apps/ech.c
+++ b/apps/ech.c
@@ -187,9 +187,13 @@ int ech_main(int argc, char **argv)
if (es == NULL)
goto end;
if (mode == OSSL_ECH_KEYGEN_MODE) {
+ if (public_name == NULL) {
+ BIO_printf(bio_err, "public_name required\n");
+ goto end;
+ }
if (verbose)
BIO_printf(bio_err, "Calling OSSL_ECHSTORE_new_config\n");
- if ((ecf = BIO_new_file(outfile, "w")) == NULL
+ if ((ecf = bio_open_owner(outfile, FORMAT_PEM, 1)) == NULL
|| OSSL_ECHSTORE_new_config(es, ech_version, max_name_length,
public_name, hpke_suite)
!= 1
@@ -207,10 +211,8 @@ int ech_main(int argc, char **argv)
for (i = 0; i != numinfiles; i++) {
if ((ecf = BIO_new_file(infiles[i], "r")) == NULL
|| OSSL_ECHSTORE_read_pem(es, ecf, OSSL_ECH_FOR_RETRY) != 1) {
- if (verbose)
- BIO_printf(bio_err, "OSSL_ECHSTORE_read_pem error for %s\n",
- infiles[i]);
- /* try read it as an ECHConfigList */
+ BIO_printf(bio_err, "OSSL_ECHSTORE_read_pem error: %s\n",
+ infiles[i]);
goto end;
}
BIO_free(ecf);
@@ -226,7 +228,8 @@ int ech_main(int argc, char **argv)
BIO_printf(bio_err, "Selected entry: %d\n", select);
if ((ecf = BIO_new_file(outfile, "w")) == NULL
|| OSSL_ECHSTORE_write_pem(es, select, ecf) != 1) {
- BIO_printf(bio_err, "OSSL_ECHSTORE_write_pem error\n");
+ BIO_printf(bio_err, "OSSL_ECHSTORE_write_pem error: %s\n",
+ outfile);
goto end;
}
if (verbose)
@@ -240,7 +243,7 @@ int ech_main(int argc, char **argv)
if (OSSL_ECHSTORE_num_entries(es, &oi_cnt) != 1)
goto end;
if (verbose)
- BIO_printf(bio_err, "Printing %d ECHConfigList\n", oi_cnt);
+ BIO_printf(bio_err, "Printing %d ECHConfig values\n", oi_cnt);
for (oi_ind = 0; oi_ind != oi_cnt; oi_ind++) {
time_t secs = 0;
char *pn = NULL, *ec = NULL;
@@ -253,9 +256,10 @@ int ech_main(int argc, char **argv)
OPENSSL_free(ec);
goto end;
}
- BIO_printf(bio_err, "ECH entry: %d public_name: %s age: %lld%s\n",
+ BIO_printf(bio_err, "ECH entry: %d public_name: %s age: %lld%s%s\n",
oi_ind, pn, (long long)secs,
- has_priv ? " (has private key)" : "");
+ has_priv ? " (has private key)" : "",
+ for_retry ? " (will be sent in retry-configs)" : "");
BIO_printf(bio_err, "\t%s\n", ec);
OPENSSL_free(pn);
OPENSSL_free(ec);
diff --git a/apps/s_server.c b/apps/s_server.c
index 91880c33a5..0b6988b873 100644
--- a/apps/s_server.c
+++ b/apps/s_server.c
@@ -1575,6 +1575,13 @@ static int ech_load_dir(SSL_CTX *lctx, const char *thedir,
BIO *in = NULL;
int loaded = 0;
+ /*
+ * If you change the output to bio_s_out here you may
+ * also need to change test/recipes/82-test_ech_client_server.t
+ * as that test checks the server's stdout to decide if the
+ * server started ok or not. Text sent to stderr won't affect
+ * that test.
+ */
if ((elen + 7) >= PATH_MAX) { /* too long, go away */
BIO_printf(bio_err, "'%s' too long - exiting\n", thedir);
return 0;
@@ -1599,11 +1606,15 @@ static int ech_load_dir(SSL_CTX *lctx, const char *thedir,
#else
r = BIO_snprintf(filepath, sizeof(filepath), "%s/%s", thedir, thisfile);
#endif
+ if (app_isdir(filepath) > 0) {
+ if (s_debug)
+ BIO_printf(bio_err, "Skipping directory: %s\n", filepath);
+ continue;
+ }
if (r < 0
- || app_isdir(filepath) > 0
|| (in = BIO_new_file(filepath, "r")) == NULL
|| OSSL_ECHSTORE_read_pem(es, in, for_retry) != 1) {
- BIO_printf(bio_err, "Failed reading from: %s\n", thisfile);
+ BIO_printf(bio_err, "Failed reading from: %s\n", filepath);
continue;
}
BIO_free_all(in);
diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md
index e980803514..1edf710e76 100644
--- a/doc/designs/ech-api.md
+++ b/doc/designs/ech-api.md
@@ -1,17 +1,11 @@
Encrypted ClientHello (ECH) APIs
================================
-TODO(ECH): replace references/links to the [sftcd
-ECH-draft-13c](https://github.com/sftcd/openssl/tree/ECH-draft-13c) (the branch
-that has good integration and interop) with relative links as files are
-migrated into (PRs for) the feature branch.
+The ECH [feature branch](https://github.com/openssl/openssl/tree/feature/ech).
+has an implementation of Encrypted Client Hello (ECH) and these are design
+notes for the APIs implemented there.
-The `OSSL_ECHSTORE` related text here matches the ECH
-[feature branch](https://github.com/openssl/openssl/tree/feature/ech).
-
-There is an [OpenSSL fork](https://github.com/sftcd/openssl/tree/ECH-draft-13c)
-that has an implementation of Encrypted Client Hello (ECH) and these are design
-notes taking the APIs implemented there as a starting point.
+This text was last updated on 2025-11-20.
The ECH Protocol
----------------
@@ -27,8 +21,8 @@ ECH makes use of [HPKE](https://datatracker.ietf.org/doc/rfc9180/) for the
encryption of the inner CH. HPKE code was merged to the master branch in
November 2022.
-The ECH APIs are also documented
-[here](https://github.com/sftcd/openssl/blob/ECH-draft-13c/doc/man3/SSL_ech_set1_echconfig.pod).
+The ECH APIs are documented
+[here](../../doc/man3/SSL_set1_echstore.pod)
The descriptions here are less formal and provide some justification for the
API design.
@@ -37,40 +31,38 @@ error. All APIs call `SSLfatal` or `ERR_raise` macros as appropriate before
returning an error.
Prototypes are mostly in
-[`include/openssl/ech.h`](https://github.com/sftcd/openssl/blob/ECH-draft-13c/include/openssl/ech.h)
-for now.
+[`include/openssl/ech.h`](../../include/openssl/ech.h).
General Approach
----------------
-This ECH implementation has been prototyped via integrations with curl, apache2,
-lighttpd, nginx and haproxy. The implementation interoperates with all other
+This ECH implementation was prototyped via integrations with curl, apache2,
+lighttpd, nginx, freenginx and haproxy. The implementation interoperates with all other
known ECH implementations, including browsers, the libraries they use
(NSS/BoringSSL), a closed-source server implementation (Cloudflare's test
-server) and with wolfssl and (reportedly) a rusttls client.
+server) and with wolfssl and rusttls.
-To date, the approach taken has been to minimise the application layer code
+The approach taken has been to minimise the application layer code
changes required to ECH-enable those applications. There is of course a tension
between that minimisation goal and providing generic and future-proof
interfaces.
-In terms of implementation, it is expected (and welcome) that many details of
-the current ECH implementation will change during review.
-
-Specification
--------------
+ECH Specification
+-----------------
ECH is an IETF TLS WG specification. It has been stable since
[draft-13](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/13/), published
in August 2021. The latest draft can be found
[here](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/).
+The specification is currently in the RFC editor's queue and is
+part of a [cluster](https://www.rfc-editor.org/cluster_info.php?cid=C430)
+of related drafts that will be published together.
-Once browsers and others have done sufficient testing the plan is to
-proceed to publishing ECH as an RFC.
-
-The only current ECHConfig version supported is 0xfe0d which will be the
+The only current ECHConfig version defined is 0xfe0d which will be the
value to be used in the eventual RFC when that issues. (We'll replace the
XXXX with the relevant RFC number once that's known.)
+TODO(ECH): Update XXXX when RFC published, and check other occurrences
+of XXXX throughout the source tree.
```c
/* version from RFC XXXX */
@@ -88,11 +80,23 @@ Note that 0xfe0d is also the value of the ECH extension codepoint:
The uses of those should be correctly differentiated in the implementation, to
more easily avoid problems if/when new versions are defined.
-Minimal Sample Code
+ECH PEM file format
-------------------
-TODO(ECH): This sample code has only been compiled. The `OSSL_ECHSTORE` stuff
-doesn't work yet.
+Servers supporting ECH need to read a set of ECH private keys and
+ECHConfigLists from storage. There is a specification for a
+[PEM file format for ECH](https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/)
+that is supported by the library. That specification is being
+processed within the IETF as an area-director sponsored draft,
+so is not a TLS WG work item, but will be an IETF stream
+RFC when completed.
+
+This PEM file format is supported by code for a number of TLS servers,
+including (at the time of writing) lighttpd, freenginx, apache2 and haproxy.
+ECH support in those servers is currently an experimental feature or similar.
+
+Minimal Sample Code
+-------------------
OpenSSL includes code for an [`sslecho`](../../demos/sslecho) demo. We've
added a minimal [`echecho`](../../demos/sslecho/echecho.c) that shows how to
@@ -122,7 +126,7 @@ packets. Some TLS handshake messages can however reduce the size of the
anonymity-set due to message-sizes. In particular the Certificate message size
will depend on the name of the SNI from the inner ClientHello. TLS however does
allow for record layer padding which can reduce the impact of underlying
-message sizes on the size of the anonymity set. The recently added
+message sizes on the size of the anonymity set. The
`SSL_CTX_record_padding_ex()` and `SSL_record_padding_ex()` APIs allow for
setting separate padding sizes for the handshake messages, (that most affect
ECH), and application data messages (where padding may affect efficiency more).
@@ -131,7 +135,7 @@ ECHConfig Extensions
--------------------
The ECH protocol supports extensibility [within the ECHConfig
-structure](https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html#section-4.2)
+structure](https://www.ietf.org/archive/id/draft-ietf-tls-esni-25.html#name-configuration-extensions)
via a typical TLS type, length, value scheme. However, to date, there are no
extensions defined, nor do other implementations provide APIs for adding or
manipulating ECHConfig extensions. We therefore take the same approach here.
@@ -149,18 +153,15 @@ no API support for generating such, and the library has no support for any
specific ECHConfig extension type. (Other than skipping over or failing as
described above.)
-In general, the ECHConfig extensibility mechanism seems to have no proven
+In general, the ECHConfig extensibility mechanism seems to have little proven
utility. (If new fields for an ECHConfig are required, a new ECHConfig version
-with the proposed changes can just as easily be developed/deployed.)
+with the proposed changes could just as easily be developed/deployed.)
The theory for ECHConfig extensions is that such values might be used to
control the outer ClientHello - controls to affect the inner ClientHello, when
ECH is used, are envisaged to be published as SvcParamKey values in SVCB/HTTP
resource records in the DNS.
-To repeat though: after a number of years of the development of ECH, no such
-ECHConfig extensions have been proposed.
-
Should some useful ECHConfig extensions be defined in future, then the
`OSSL_ECHSTORE` APIs could be extended to enable management of such, or, new
opaque types could be developed enabling further manipulation of ECHConfig and
@@ -186,7 +187,8 @@ used to authenticate servers. Notably:
- In particular, the above means that we do not see any need to repeatedly
parse or process related ECHConfigList structures - each can be processed
- independently for all practical purposes.
+ independently for all practical purposes, and there is no equivalent to
+ X.509 path processing.
- There are all the usual algorithm variations, and those will likely result in
the same x25519 versus p256 combinatorics. How that plays out has yet to be
@@ -194,6 +196,14 @@ used to authenticate servers. Notably:
wise to be agnostic and support all relevant combinations. (And doing so is not
that hard.)
+- At the time of writing there is work ongoing to specify use of post-quantum
+ KEMs with HPKE. Once that work matures, and the relevant (hybrid) KEMs are
+ supported by OpenSSL, then they should be usable with ECH. It is quite likely
+ at least some test code will need changes due to the increase in the size
+ of the ECH extension. For now, there is no support for e.g. use of an
+ equivalent to X25519MLKEM768 for ECH encryption. ECH does work fine if
+ X25519MLKEM768 is used for the TLS key exchange.
+
ECH Store APIs
--------------
@@ -236,8 +246,7 @@ int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age);
`OSSL_ECHSTORE_new_config()` allows the caller to create a new private key
value and the related "singleton" ECHConfigList structure.
`OSSL_ECHSTORE_write_pem()` allows the caller to produce a "PEM" data
-structure (conforming to the [PEMECH
-specification](https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/))
+structure (conforming to the ECH PEM file format)
from the `OSSL_ECHSTORE` entry identified by the `index`. (An `index` of
`OSSL_ECHSTORE_LAST` will select the last entry. An `index` of
`OSSL_ECHSTORE_ALL` will output all public values, and no private values.)
@@ -282,9 +291,10 @@ when these functions succeed. Any previously associated `OSSL_ECHSTORE`
will be `OSSL_ECHSTORE_free()`ed.
There is also an API that allows setting an ECHConfigList for an SSL
-connection, that is compatible with BoringSSL. Note that the input
-`ecl` here can be either base64 or binary encoded, but for
-BoringSSL it must be binary encoded.
+connection, that is compatible with BoringSSL, leading to smaller code changes
+for clients that support OpenSSL or BoringSSL. Note that the input `ecl` here
+for OpenSSL can be either base64 or binary encoded, but for BoringSSL it must
+be binary encoded.
```c
int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len);
@@ -316,7 +326,7 @@ typedef struct ossl_echext_st {
DEFINE_STACK_OF(OSSL_ECHEXT)
typedef struct ossl_echstore_entry_st {
- uint16_t version; /* 0xff0d for draft-13 */
+ uint16_t version; /* 0xfe0d for RFC XXXX */
char *public_name;
size_t pub_len;
unsigned char *pub;
@@ -377,17 +387,17 @@ In addition to the obvious fields from each ECHConfig, we also store:
- The `EVP_PKEY` pointer to the private key value associated with the
relevant ECHConfig, for use by servers.
-- The PEM filename and file modification time from which a private key value
- and ECHConfigList were loaded. If those values are loaded from memory,
- the filename value is the SHA-256 hash of the encoded ECHConfigList and
- the load time is the time of loading. These values assist when servers
- periodically re-load sets of files or PEM structures from memory.
+- The time at which a private key value and/or ECHConfigList were loaded.
+ This value is useful when servers periodically re-load sets of files
+ or PEM structures from memory, e.g. for the haproxy server.
Split-mode handling
-------------------
TODO(ECH): This ECH split-mode API should be considered tentative. It's design
-will be revisited as we get to considering the internals.
+should be revisited now, and either omitted from the initial release that'd
+only support shared-mode ECH, or else (better:-), agreed and included in the
+same time frame.
ECH split-mode involves a front-end server that only does ECH decryption and
then passes on the decrypted inner CH to a back-end TLS server that negotiates
@@ -541,105 +551,166 @@ The following options are defined for ECH and may be set via
`SSL_set_options()`:
```c
-/* set this to tell client to emit greased ECH values when not doing
- * "real" ECH */
-#define SSL_OP_ECH_GREASE SSL_OP_BIT(36)
-/* If this is set then the server side will attempt trial decryption */
-/* of ECHs even if there is no matching record_digest. That's a bit */
-/* inefficient, but more privacy friendly */
-#define SSL_OP_ECH_TRIALDECRYPT SSL_OP_BIT(37)
-/* If set, clients will ignore the supplied ECH config_id and replace
- * that with a random value */
-#define SSL_OP_ECH_IGNORE_CID SSL_OP_BIT(38)
-/* If set, servers will add GREASEy ECHConfig values to those sent
- * in retry_configs */
-#define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(39)
+/* Set this to tell client to emit greased ECH values */
+# define SSL_OP_ECH_GREASE SSL_OP_BIT(37)
+/*
+ * If this is set then the server side will attempt trial decryption
+ * of ECHs even if there is no matching ECH config_id. That's a bit
+ * inefficient, but more privacy friendly.
+ */
+# define SSL_OP_ECH_TRIALDECRYPT SSL_OP_BIT(38)
+/*
+ * If set, clients will ignore the supplied ECH config_id and replace
+ * that with a random value.
+ */
+# define SSL_OP_ECH_IGNORE_CID SSL_OP_BIT(39)
+/*
+ * If set, servers will add GREASEy ECHConfig values to those sent
+ * in retry_configs.
+ */
+# define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(40)
```
Build Options
-------------
-All ECH code is protected via `#ifndef OPENSSL_NO_ECH` and there is
-a `no-ech` option to build without this code.
-
-BoringSSL APIs
---------------
-
-Brief descriptions of BoringSSL APIs are below together with initial comments
-comparing those to the above. (It may be useful to consider the extent to
-which it is useful to make OpenSSL and BoringSSL APIs resemble one another.)
-
-Just as our implementation is under development, BoringSSL's
-`include/openssl/ssl.h` says: "ECH support in BoringSSL is still experimental
-and under development."
-
-### GREASE
+Almost all ECH code is protected via `#ifndef OPENSSL_NO_ECH` and there is a
+`no-ech` option to build without this code.
-BoringSSL uses an API to enable GREASEing rather than an option.
+Applications using ECH may choose to detect the availability of ECH in
+the library by checking that `SSL_OP_ECH_GREASE` is defined. This is
+used by some server applications today.
-```c
-OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
-```
-
-This could work as well for our implementation, or BoringSSL could probably
-change to use an option, unless there's some reason to prefer not adding new
-options.
-
-### Verifying the outer CH rather than inner
-
-BoringSSL seems to use this API to change the DNS name being verified in order
-to validate a `retry_config`.
+ECH Tests
+---------
-```c
-OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl,
- const char **out_name,
- size_t *out_name_len);
+The following tests are included in the `make test` target:
+
+- [`test_app_ech`](../../test/recipes/20-test_app_ech.t)
+- [`test_ech`](../../test/ech_test.c)
+- [`test_ech_corrupt`](../../test/ech_corrupt_test.c)
+- [`test_ech_client_server`](../../test/recipes/82-test_ech_client_server.t)
+
+There are also two external tests to check interoperability
+with the NSS and BoringSSL libraries:
+
+- [`test_external_ech_nss`](../../test/recipes/95-test_external_ech_nss.t)
+- [`test_external_ech_bssl`](../../test/recipes/95-test_external_ech_bssl.t)
+
+The `test_app_ech` test exercises the `openssl ech` command line utility that
+can be used to generate and manipulate ECH keys and configurations.
+
+The `test_ech` test exercises ECH APIs, including round-trip tests that use ECH
+in TLS sessions. The code for this includes many valid and invalid test vectors
+and is designed to be relatively easily extended with additional tests.
+
+`test_ech_corrupt` is modelled on [sslcorruptetst.c](../../test/sslcorrupttest.c)
+and mainly includes tests where variously incorrectly encoded inner ClientHello
+test vectors are encrypted using HPKE and then successfully decrypted by a
+server that then rejects the connection returning the expected error code.
+
+`test_ech_client_server` exercises the various ECH command line
+options for the OpenSSL `s_client` and `s_server` commands. Changes to
+the output from those command may require changes to these tests as
+they use pattern matching on the outputs to detect expected successes
+or failures.
+
+The external tests check that the library correctly interoperates,
+as a client or server, with NSS or BoringSSL. These require a build
+configured with `enable-external-tests` and are quite time consuming
+when first run, as they need to download and build the relevant
+NSS or BoringSSL library. The client/server tests here are not very
+extensive and just check that a basic configuration interoperates.
+
+How to measure coverage of ECH tests
+------------------------------------
+
+There are likely many ways to do this, but the following is the
+recipe used during ECH development:
+
+```bash
+./config --debug enable-external-tests --coverage no-asm no-afalgeng no-shared -DPEDANTIC -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+make -s -j12
+make test TESTS='test_ech test_ech_corrupt test_app_ech test_ech_client_server test_external_ech_bssl test_external_ech_nss'
+# next line failed, was replaced the the one following
+# lcov -d . -c -o ./lcov.info
+/usr/bin/geninfo . --output-filename ./lcov.info --memory 0 --ignore-errors mismatch
+genhtml ./lcov.info --output-directory $HOME/tmp/myco
```
-I'm not sure how this compares. Need to investigate.
-
-### Create an ECHConfigList
+To clean away the coverage files:
-The first function below outputs an ECHConfig, the second adds one of those to
-an `SSL_ECH_KEYS` structure, the last emits an ECHConfigList from that
-structure. There are other APIs for managing memory for `SSL_ECH_KEYS`
-
-These APIs also expose HPKE to the application via `EVP_HPKE_KEY` which is
-defined in `include/openssl/hpke.h`. HPKE handling differs quite a bit from
-the HPKE APIs merged to OpenSSL.
-
-```c
-OPENSSL_EXPORT int SSL_marshal_ech_config(uint8_t **out, size_t *out_len,
- uint8_t config_id,
- const EVP_HPKE_KEY *key,
- const char *public_name,
- size_t max_name_len);
-OPENSSL_EXPORT int SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config,
- const uint8_t *ech_config,
- size_t ech_config_len,
- const EVP_HPKE_KEY *key);
-OPENSSL_EXPORT int SSL_ECH_KEYS_marshal_retry_configs(const SSL_ECH_KEYS *keys,
- uint8_t **out,
- size_t *out_len);
+```bash
+find . -name '*.gcda' -exec rm {} \;
+find . -name '*.gcno' -exec rm {} \;
+rm lcov.info
+make clean
+make -j12
```
-Collectively these are similar to `OSSL_ECH_make_echconfig()`.
-
-### Setting ECH keys on a server
-
-Again using the `SSL_ECH_KEYS` type and APIs, servers can build up a set of
-ECH keys using:
+Checking memory errors
+----------------------
-```c
-OPENSSL_EXPORT int SSL_CTX_set1_ech_keys(SSL_CTX *ctx, SSL_ECH_KEYS *keys);
+As is typical with the library there is a good bit of code that handles error
+cases that are hard to test. Causing memory allocation failures though allows
+us to get at most of those code fragments.
+
+To get to those error handling lines of code, one can use an exhaustive script
+that incrementally allows more and more memory allocations to work before
+triggering failures such as:
+
+```bash
+#!/bin/bash
+#
+# run 16k tests
+
+# if you want tracing
+# export OPENSSL_TRACE="TLS"
+logfile=loads.log
+iter=0
+
+function whenisitagain()
+{
+ /bin/date -u +%Y%m%d-%H%M%S
+}
+NOW=$(whenisitagain)
+echo "==================================" >>$logfile
+echo "Started at $NOW" >>$logfile
+
+# 16,000 calls without failures should be enough for the
+# test to pass - gprof says we have 15,558 calls to
+# CRYPTO_malloc for the test below
+while ((iter < 16000))
+do
+ echo "Doing $iter" >>$logfile
+ iter=$((iter+1))
+ export OPENSSL_MALLOC_FAILURES="$iter@0;0@99;"
+ ./test/ech_test -test 6 -iter 1 >>$logfile 2>&1
+ echo "Done $iter"
+ echo "Done $iter" >>$logfile
+ echo "" >>$logfile
+ echo "" >>$logfile
+done
+
+NOW=$(whenisitagain)
+echo "Ended at $NOW" >>$logfile
+echo "==================================" >>$logfile
```
-### Getting status
+An `OPENSSL_MALLOC_FAILURES` value of `100@0;0@99` means to allow the first 100
+memory allocations to work, and to then switch to a mode where there's a 99%
+chance of memory allocation failing. By using `$iter` we just keep incrementing
+how far into the run we allow nominal memory allocation before we break things,
+which should result in (eventually:-) hitting every possible memory allocation
+failure handling line of code.
-BoringSSL has:
+You need to build with the `crypto-mdebug` option to get the memory allocation
+failure, so that'd be something like:
-```c
-OPENSSL_EXPORT int SSL_ech_accepted(const SSL *ssl);
+```bash
+./config --debug enable-external-tests enable-crypto-mdebug --coverage no-asm no-afalgeng no-shared -DPEDANTIC -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
```
-That seems to be a subset of `SSL_ech_get1_status()`.
+The script above gets us from 75% lines of code covered based on normal tests
+to 85.6% for the `ech_internal.c` file, which is our least well covered by
+normal tests. The script takes about 20 minutes to run on a developer laptop.
diff --git a/doc/man1/openssl-ech.pod.in b/doc/man1/openssl-ech.pod.in
index 5abe0ae5b0..e2a91b2118 100644
--- a/doc/man1/openssl-ech.pod.in
+++ b/doc/man1/openssl-ech.pod.in
@@ -103,7 +103,7 @@ L<SSL_set1_echstore(3)>
=head1 HISTORY
-This functionality described here was added in OpenSSL 3.5.
+This functionality described here was added in OpenSSL 4.0.
=head1 COPYRIGHT
diff --git a/doc/man1/openssl-s_client.pod.in b/doc/man1/openssl-s_client.pod.in
index 81ca0534de..7ecfc1f3b6 100644
--- a/doc/man1/openssl-s_client.pod.in
+++ b/doc/man1/openssl-s_client.pod.in
@@ -192,8 +192,8 @@ Specifies the ECHConfigList value to use for Encrypted Client Hello (ECH) for
the TLS session. The value must be a base64 encoded ECHConfigList.
The ECHConfigList structure is defined in RFC XXXX. (That's currently in
-L<https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/>)
-=for comment TODO(ECH): replace XXXX when RFC published.
+L<https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni/>.)
+TODO(ECH): replace XXXX when RFC published.
=item B<-proxy_user> I<userid>
diff --git a/doc/man3/SSL_new.pod b/doc/man3/SSL_new.pod
index d01996fba1..0edf334901 100644
--- a/doc/man3/SSL_new.pod
+++ b/doc/man3/SSL_new.pod
@@ -94,6 +94,8 @@ SSL_set0_client_CA_list() or similar functions
=item any client certificate types configured via SSL_set1_client_certificate_types
+=item any Encrypted Client Hello (ECH) settings (see L<SSL_set1_echstore(3)>).
+
=back
SSL_dup() is not supported on QUIC SSL objects and returns NULL if called on
diff --git a/doc/man3/SSL_set1_echstore.pod b/doc/man3/SSL_set1_echstore.pod
index 05034d3102..39b5efbce1 100644
--- a/doc/man3/SSL_set1_echstore.pod
+++ b/doc/man3/SSL_set1_echstore.pod
@@ -14,7 +14,7 @@ SSL_CTX_get1_echstore, SSL_get1_echstore, SSL_ech_set1_server_names,
SSL_ech_set1_outer_server_name, SSL_ech_set1_outer_alpn_protos,
SSL_ech_get1_status, SSL_ech_set1_grease_suite, SSL_ech_set_grease_type,
SSL_ech_set_callback, SSL_ech_get1_retry_config,
-SSL_CTX_ech_set1_outer_alpn_protos, SSL_CTX_ech_raw_decrypt,
+SSL_CTX_ech_set1_outer_alpn_protos,
SSL_CTX_ech_set_callback,SSL_set1_ech_config_list
- Encrypted Client Hello (ECH) functions
@@ -29,9 +29,9 @@ SSL_CTX_ech_set_callback,SSL_set1_ech_config_list
const char *public_name, OSSL_HPKE_SUITE suite);
int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out);
int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in);
- int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs,
- char **public_name, char **echconfig,
- int *has_private, int *for_retry);
+ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs,
+ char **public_name, char **echconfig,
+ int *has_private, int *for_retry);
int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index);
int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv,
BIO *in, int for_retry);
@@ -53,12 +53,6 @@ SSL_CTX_ech_set_callback,SSL_set1_ech_config_list
int SSL_ech_set_grease_type(SSL *s, uint16_t type);
void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f);
int SSL_ech_get1_retry_config(SSL *s, unsigned char **ec, size_t *eclen);
- int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx,
- int *decrypted_ok,
- char **inner_sni, char **outer_sni,
- unsigned char *outer_ch, size_t outer_len,
- unsigned char *inner_ch, size_t *inner_len,
- unsigned char **hrrtok, size_t *toklen);
void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f);
int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx,
const unsigned char *protos,
@@ -67,42 +61,177 @@ SSL_CTX_ech_set_callback,SSL_set1_ech_config_list
=head1 DESCRIPTION
-TODO(ECH): Complete this text...
+TODO(ECH): Replace XXXX and links to I-D when RFC issued.
The Encrypted Client Hello (ECH) APIs described here are built around
-the concept of an `OSSL_ECHSTORE` which contains ECH configuration
-information relevant for the current 'SSL_CTX' or 'SSL' connection.
-
-Mention SSL_set1_echstore() is a thing
-Mention OSSL_ECHSTORE_new() is a thing
-Mention OSSL_ECHSTORE_free() is a thing
-Mention OSSL_ECHSTORE_new_config() is a thing
-Mention OSSL_ECHSTORE_write_pem() is a thing
-Mention OSSL_ECHSTORE_read_echconfiglist() is a thing
-Mention OSSL_ECHSTORE_get1_info() is a thing
-Mention OSSL_ECHSTORE_downselect() is a thing
-Mention OSSL_ECHSTORE_set1_key_and_read_pem() is a thing
-Mention OSSL_ECHSTORE_read_pem() is a thing
-Mention OSSL_ECHSTORE_num_keys() is a thing
-Mention OSSL_ECHSTORE_num_entries() is a thing
-Mention OSSL_ECHSTORE_flush_keys() is a thing
-Mention SSL_CTX_set1_echstore() is a thing
-Mention SSL_CTX_get1_echstore() is a thing
-Mention SSL_get1_echstore() is a thing
-Mention SSL_ech_set1_server_names() is a thing
-Mention SSL_ech_set1_outer_server_name() is a thing
-Mention SSL_ech_set1_outer_alpn_protos() is a thing
-Mention SSL_ech_get1_status() is a thing
-Mention SSL_ech_set1_grease_suite() is a thing
-Mention SSL_ech_set_grease_type() is a thing
-Mention SSL_ech_set_callback() is a thing
-Mention SSL_ech_get1_retry_config() is a thing
-Mention SSL_CTX_ech1_set_outer_alpn_protos() is a thing
-Mention SSL_CTX_ech_raw_decrypt() is a thing
-Mention SSL_CTX_ech_set_callback() is a thing
-Mention SSL_set1_ech_config_list() is a thing
-
-=head2 Callback Function
+the concept of an B<OSSL_ECHSTORE> which contains ECH configuration
+information relevant for an B<SSL_CTX> or B<SSL> connection.
+
+This release only supports ECH shared-mode and has no support for
+ECH split-mode.
+
+=head2 OSSL_ECHSTORE APIs
+
+The externally opaque type B<OSSL_ECHSTORE> allows applications to create and
+manage ECHConfigList values, ECH private keys and associated meta-data. The
+external APIs using B<OSSL_ECHSTORE> are:
+
+OSSL_ECHSTORE_new() and OSSL_ECHSTORE_free() create and free the
+internal storage required.
+
+OSSL_ECHSTORE_new_config() allows the caller to create a new private key
+value and a related "singleton" ECHConfigList structure.
+("Singleton" meaning the ECHConfigList only contains one public key.)
+The I<echversion> is the ECHConfig version to use, which is typically
+B<OSSL_ECH_CURRENT_VERSION> with a value of 0xfe0d. The I<max_name_length>
+specifies the maximum known DNS name length that will be present in an ECH
+extension (if known) and is used for ECH padding but should typically be zero,
+to indicate no known maximum. The I<public_name> is the DNS name to use in
+the ECHConfig I<public_name> field, and the I<suite> specifies the
+B<OSSL_HPKE_suite> to use (see L<OSSL_HPKE_CTX_new(3)> for details.)
+
+OSSL_ECHSTORE_write_pem() allows the caller to produce an ECH PEM data structure
+(conforming to the ECH PEM file format) from the B<OSSL_ECHSTORE> entry
+identified by the I<index>. (An I<index> of B<OSSL_ECHSTORE_LAST> will select the
+last entry. An I<index> of B<OSSL_ECHSTORE_ALL> will output all public values,
+and no private values, otherwise the I<index> selects a specific entry with
+zero selecting the first entry.) The output will be written to the I<out>
+B<BIO>.
+
+The B<OSSL_ECHSTORE> APIs above will typically be used via the "openssl ech"
+command line tool.
+
+OSSL_ECHSTORE_read_echconfiglist() reads from the I<in> B<BIO> and parses a
+base64-encoded ECHConfigList value normally found in the "ech=" SvcParamKey
+present in an SVCB or HTTPS RR retrieved from the DNS. The resulting set of
+ECHConfig values, are associated with the I<es> store, and can then be
+associated with an B<SSL_CTX> or B<SSL> structure for TLS client connections.
+
+Input B<BIO> values (such a I<in> above) must not use unterminated B<BIO>'s.
+
+OSSL_ECHSTORE_get1_info() queries the store provided by I<es> and finds the
+entry within the store specified by I<index>. The number of seconds since that
+entry was first generated or loaded/decoded is stored in I<loaded_secs>, and
+the related public name is stored in I<*public_name>. This function also
+outputs a string useful for logging or display (as described in the
+L</String form of ECHConfig> section) and stores that in I<*echconfig>. It is
+the callers responsibility to free the strings returned in I<public_name> and
+I<echconfig> after they have been returned. If the specified entry has a
+private key associated with it then I<has_private> will be set to 1, otherwise
+it will be set to 0. Similarly if the ECHConfig for this entry will be included
+in "retry-configs" then the I<for_retry> will be set to 1 or zero if that
+ECHConfig will not be included in retry-configs.
+
+The ECH fallback scheme involving retry-configs is described at
+L<https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-25#section-6.1.6>.
+
+=begin comment
+
+TODO(ECH): The I-D above should be replaced with the RFC when published.
+
+=end comment
+
+OSSL_ECHSTORE_downselect() provides the caller a way to select one particular
+ECHConfig value based on the zero-based I<index> from those stored in the
+I<es>, discarding the rest. This can be used by a client (via L<openssl-ech(1)>
+and L<openssl-s_client(1)> or equivalent) to pick a specific ECHConfig to use
+in a TLS connection.
+
+OSSL_ECHSTORE_set1_key_and_read_pem() and OSSL_ECHSTORE_read_pem() can be used
+to load a private key value and associated ECHConfigList from the B<BIO> I<in>
+into an B<OSSL_ECHSTORE> structure. The former function pairs a
+previously-loaded private key (in B<EVP_PKEY> format) with an associated
+base64-encoded ECHConfigList in the I<in> B<BIO>. The latter function reads
+both from an ECH PEM file. Those can be used (by servers) to enable ECH for an
+B<SSL_CTX> or B<SSL> connection. In addition to loading those values, the
+application can also indicate via I<for_retry> which ECHConfig values are to be
+included in the I<retry_configs> fallback scheme defined by the ECH protocol.
+
+An ECH PEM file may contain a private key and an ECHConfigList with more than
+one ECHConfig, for example if different public keys, I<public_name> values, or
+AEAD/KDF settings are to be supported. When such a file is read, the resulting
+B<OSSL_ECHSTORE> will contain one entry for each ECHConfig in the
+ECHConfigList, so will be presented to applications as a set of "singleton"
+ECHConfig values, with the private key associated with each matching public key
+value.
+
+OSSL_ECHSTORE_num_entries() and OSSL_ECHSTORE_num_keys() allow an application
+to see how many ECH configs (in I<numentries>) and private keys (in
+I<numkeys>) are present in the I<es> B<OSSL_ECHSTORE> store.
+
+OSSL_ECHSTORE_flush_keys() allows a server to flush keys from I<es> that were
+loaded more than I<age> seconds ago. The general model is that a server can
+maintain an B<OSSL_ECHSTORE> into which it periodically loads the "latest" set
+of keys, e.g. hourly, and also discards the keys that are too old, e.g. more
+than 3 hours old. This allows for more robust private key management even if
+public key distribution suffers temporary failures.
+
+SSL_CTX_set1_echstore() and SSL_set1_echstore() allow clients and servers to
+associate I<es> (an B<OSSL_ECHSTORE>) with an B<SSL_CTX> or B<SSL> structure.
+ECH will be enabled for the relevant B<SSL_CTX> or B<SSL> connection when these
+functions succeed. Any previously associated B<OSSL_ECHSTORE> will be freed via
+OSSL_ECHSTORE_free(). Internally, B<OSSL_ECHSTORE> values within an B<SSL_CTX>
+or B<SSL> connection are deep-copied, and are not refcounted.
+
+SSL_CTX_get1_echstore() and SSL_get1_echstore() provide access to the
+B<OSSL_ECHSTORE> associated with an B<SSL_CTX> or B<SSL> connection. The
+returned B<OSSL_ECHSTORE> can be modified and then re-associated with an
+B<SSL_CTX> or B<SSL> connection.
+
+=head2 Client ECH Controls
+
+SSL_set1_ech_config_list() allows clients to setup ECH by associating an
+ECHConfigList, I<ecl> with an B<SSL> connection I<s>. This is compatible with
+current BoringSSL APIs, allowing for smaller code changes for clients that
+support OpenSSL or BoringSSL. Note that the input I<ecl> here for OpenSSL can
+be either base64 or binary encoded, but for BoringSSL it must be binary
+encoded.
+
+SSL_ech_set1_server_names() and SSL_ech_set1_outer_server_name() allow clients
+to more directly control the values to be used for inner and outer Server Name
+Indication (SNI) values for an B<SSL> connection, I<s>. The I<inner_name>
+provided will be used as with L<SSL_set_tlsext_host_name(3)> to populate the
+SNI value for the inner ClientHello. The I<outer_name>, if non-NULL, can
+over-ride the public_name field of the ECHConfig used for the connection. The
+I<no_outer> input allows a client to emit an outer ClientHello with no SNI at
+all. Providing a NULL for the I<outer_name> means to send the I<public_name>
+provided from the ECHConfigList unless the I<no_outer> provided has the value
+1.
+
+If a client has called L<SSL_CTX_set_alpn_protos(3)> or
+L<SSL_set_alpn_protos(3)> then the ALPN value will be the same in the inner and
+outer ClientHello messages. SSL_ech_set1_outer_alpn_protos() and
+SSL_CTX_ech_set1_outer_alpn_protos() allow clients to set a specific value for
+the ALPN sent in the outer ClientHello of the B<SSL> connection, I<s>. The
+I<protos> and I<protos_len> inputs must be provided as for
+L<SSL_set_alpn_protos(3)>.
+
+If a client attempts ECH but that fails, or sends an ECH-GREASE'd ClientHello,
+to an ECH-supporting server, then that server may return a set of ECH
+retry-config values that the client could choose to use in a subsequent
+connection. The client can detect this situation if SSL_ech_get1_status() for
+the B<SSL> connection I<s>, returns a status of B<SSL_ECH_STATUS_GREASE_ECH>,
+and can then access the ECH retry config values via SSL_ech_get1_retry_config()
+where the I<ec> value returned will contain a binary-encoded ECHConfigList
+of length I<eclen>.
+
+"GREASEing" (defined in RFC8701) is a mechanism intended to discourage protocol
+ossification that can be used for ECH.
+
+SSL_ech_set1_grease_suite() allows a client to GREASE ECH for the B<SSL>
+connection I<s>, using a specific B<OSSL_HPKE_SUITE> as the value for I<suite>
+(see L<OSSL_HPKE_get_grease_value(3)>). SSL_ech_set_grease_type() allows a
+client to add a GREASE'd ECH for the B<SSL> connection I<s>, using the
+specified ClientHello extension number I<type>.
+
+Clients and servers can query the status of ECH for a B<SSL> connection I<s>,
+using the SSL_ech_get1_status() function. As SNI handling is core to ECH this
+will also return the I<inner_sni> and I<outer_sni> values to be used or that
+were used as well as a status code as the return value. These name values must
+be freed by the caller. The various status values returned by this function
+are as described in L</Constants>.
+
+=head2 Callback Functions
Applications can set a callback function that will be called when the
outcome from an attempt at ECH has been determined. On the server,
@@ -114,11 +243,11 @@ function will be triggered even if the client is only GREASEing.
The callback function prototype is:
- typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str);
+ typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str);
-To set a callback function use SSL_ech_set_callback() or
-SSL_CTX_ech_set_callback() - the I<f> input should match the
-above prototype.
+To set a callback function for the B<SSL> connection I<s>, use
+SSL_ech_set_callback() or SSL_CTX_ech_set_callback() for a B<SSL_CTX> I<ctx> -
+the I<f> input should match the above prototype.
When the callback function is called, the I<str> will point at a string
intended for logging describing the state of ECH processing.
@@ -139,59 +268,154 @@ An example string I<str> as seen on a client might be:
ech_backend=0
ech_success=1
2 ECHConfig values loaded
- cfg(0): [fe0d,5d,cover.defo.ie,0020,[0001,0001],190984309c1a24cb944c005eb79d9c72ca9a4a979194b553dfd0bffc6b5c152d,00,00]
- cfg(1): [fe0d,fd,cover.defo.ie,0020,[0001,0001],46dd4e2c81bb15ef9d194c99b86983844e2a1387e4fb7e7d3b8d368c8e1b4d2a,00,00]
+ cfg(0): [fe0d,5d,cover.defo.ie,[0020,0001,0001],190984309c1a24cb944c005eb79d9c72ca9a4a979194b553dfd0bffc6b5c152d,00,00]
+ cfg(1): [fe0d,fd,cover.defo.ie,[0020,0001,0001],46dd4e2c81bb15ef9d194c99b86983844e2a1387e4fb7e7d3b8d368c8e1b4d2a,00,00]
+
+=head2 Constants
+
+Some externally visible limits:
+
+=over 4
+
+=item B<OSSL_ECH_MAX_PAYLOAD_LEN> 1500, maximum length of an ECH ciphertext to en/decode
+
+=item B<OSSL_ECH_MIN_ECHCONFIG_LEN> 32, minimum length of an encoded ECHConfig
+
+=item B<OSSL_ECH_MAX_ECHCONFIG_LEN> 1500, maximum length of an encoded ECHConfig
+
+=item B<OSSL_ECH_MAX_ECHCONFIGEXT_LEN> 512, maximum length of an ECHConfig extension
+
+=item B<OSSL_ECH_MAX_MAXNAMELEN> 255, maximum for ECHConfig max name length
+
+=item B<OSSL_ECH_MAX_PUBLICNAME> 255, maximum length of an ECHConfig public name
+
+=item B<OSSL_ECH_MAX_ALPN_LEN> 255, maximum overall length of an ALPN
+
+=item B< OSSL_ECH_OUTERS_MAX> 20, maximum number of extensions compressed via outer-exts
+
+=item B< OSSL_ECH_ALLEXTS_MAX> 32, maximum total number of extensions allowed
+
+=back
+
+ECH version - the only supported version is 0xfe0d currently.
+
+=over 4
+
+=item B<OSSL_ECH_RFCXXXX_VERSION> 0xfe0d, official ECHConfig version
+
+=item B<OSSL_ECH_CURRENT_VERSION> OSSL_ECH_RFCXXXX_VERSION
+
+=back
+
+Return codes from SSL_ech_get_status
+
+=over 4
+
+=item B<SSL_ECH_STATUS_BACKEND> 4, ECH backend: saw an ech_is_inner
+
+=item B<SSL_ECH_STATUS_GREASE_ECH> 3, GREASEd and got an ECH in return
+
+=item B<SSL_ECH_STATUS_GREASE> 2, ECH GREASE happened
+
+=item B<SSL_ECH_STATUS_SUCCESS> 1, Success
+
+=item B<SSL_ECH_STATUS_FAILED> 0, Some internal or protocol error
+
+=item B<SSL_ECH_STATUS_BAD_CALL> -100, Some in/out arguments were NULL
+
+=item B<SSL_ECH_STATUS_NOT_TRIED> -101, ECH wasn't attempted
+
+=item B<SSL_ECH_STATUS_BAD_NAME> -102, ECH ok but server cert bad
+
+=item B<SSL_ECH_STATUS_NOT_CONFIGURED> -103, ECH wasn't configured
+
+=item B<SSL_ECH_STATUS_FAILED_ECH> -105, We tried, failed and got an ECH, from a verified name
+
+=item B<SSL_ECH_STATUS_FAILED_ECH_BAD_NAME> -106, We tried, failed and got an ECH, from a bad name
+
+=back
+
+Values for I<for_retry>
+
+=over 4
+
+=item B<SSL_ECH_USE_FOR_RETRY> 1. use corresponding ECHConfig values
+
+=item B<SSL_ECH_NOT_FOR_RETRY> 0. don't use corresponding ECHConfig values
+
+=back
+
+Indexing an OSSL_ECHSTORE
+
+=over 4
+
+=item B<OSSL_ECHSTORE_LAST>, -1 select the last entry in the store
+
+=item B<OSSL_ECHSTORE_ALL>, -2 select all entries in the store, e.g. to print public values
+
+=back
+
+=head2 String form of ECHConfig
+
+OSSL_ECHSTORE_get1_info() returns the string form of the ECH public name plus
+a string describing the content of the ECHConfig for example:
+
+ [fe0d,4d,example.com,[0020,0001,0002],c103d20dddce9b4445829bf01f5b533b728bfa0ebe3a97da33574bc096bb846e,00,00]
+
+In the order presented in the example above, the I<version> is 0xfe0d, the
+I<config_id> is 0x4d, the I<public_name> is "example.com", there is one
+ciphersuite with a I<kem-id> of 0x20, I<kdf-id> is 0x01 and I<aead-id> is 0x02;
+the I<public_key> is the hexadecimal string "c1...6e", the I<max-name-length>
+is 0x00 and the I<extensions> length is 0x00 as no ECHConfig extensions are
+present.
=head1 RETURN VALUES
-All functions named here return one on success and zero on error.
-
-SSL_set1_echstore() returns zero on error
-SSL_set1_ech_config_list() returns zero on error
-OSSL_ECHSTORE_new() returns zero on error
-OSSL_ECHSTORE_free() returns zero on error
-OSSL_ECHSTORE_new_config() returns zero on error
-OSSL_ECHSTORE_write_pem() returns zero on error
-OSSL_ECHSTORE_read_echconfiglist() returns zero on error
-OSSL_ECHSTORE_get1_info() returns zero on error
-OSSL_ECHSTORE_downselect() returns zero on error
-OSSL_ECHSTORE_set1_key_and_read_pem() returns zero on error
-OSSL_ECHSTORE_read_pem() returns zero on error
-OSSL_ECHSTORE_num_keys() returns zero on error
-OSSL_ECHSTORE_num_entries() returns zero on error
-OSSL_ECHSTORE_flush_keys() returns zero on error
-SSL_CTX_set1_echstore() returns zero on error
-SSL_CTX_get1_echstore() returns zero on error
-SSL_get1_echstore() returns zero on error
-SSL_ech_set_server_names() returns zero on error
-SSL_ech_set_outer_server_name() returns zero on error
-SSL_ech_set_outer_alpn_protos() returns zero on error
-SSL_ech_get1_status() returns zero on error
-SSL_ech_set_grease_suite() returns zero on error
-SSL_ech_set_grease_type() returns zero on error
-SSL_ech_set_callback() returns zero on error
-SSL_ech_get_retry_config() returns zero on error
-SSL_CTX_ech_set1_outer_alpn_protos() returns zero on error
-SSL_CTX_ech_raw_decrypt() returns zero on error
-SSL_CTX_ech_set_callback() returns zero on error
+SSL_set1_echstore(), SSL_set1_ech_config_list(), OSSL_ECHSTORE_new_config(),
+OSSL_ECHSTORE_write_pem(), OSSL_ECHSTORE_read_echconfiglist(),
+OSSL_ECHSTORE_get1_info(), OSSL_ECHSTORE_downselect(),
+OSSL_ECHSTORE_set1_key_and_read_pem(), OSSL_ECHSTORE_read_pem(),
+OSSL_ECHSTORE_num_keys(), OSSL_ECHSTORE_num_entries(),
+OSSL_ECHSTORE_flush_keys(), SSL_CTX_set1_echstore(),
+SSL_ech_set_server_names(), SSL_ech_set_outer_server_name(),
+SSL_ech_set_outer_alpn_protos(), SSL_ech_get1_status(),
+SSL_ech_set_grease_suite(), SSL_ech_set_grease_type(),
+SSL_ech_get_retry_config() and SSL_CTX_ech_set1_outer_alpn_protos() all return
+zero on error and one on success.
+
+SSL_ech_set_callback(), SSL_CTX_ech_set_callback(), OSSL_ECHSTORE_free() have
+no return value.
+
+SSL_CTX_get1_echstore(), SSL_get1_echstore() and OSSL_ECHSTORE_new() return a
+pointer to an B<OSSL_ECHSTORE>.
Note that SSL_CTX_ech_set1_outer_alpn_protos() and
-SSL_ech_set1_outer_alpn_protos() return zero on error and 1 on success.
-This is in contrast to SSL_CTX_set1_alpn_protos() and SSL_set1_alpn_protos()
+SSL_ech_set1_outer_alpn_protos() return zero on error and 1 on success. This
+is in contrast to L<SSL_CTX_set_alpn_protos(3)> and L<SSL_set_alpn_protos(3)>
which (unusually for OpenSSL) return 0 on success and 1 on error.
=head1 SEE ALSO
-The Encrypted ClientHello specification: L<https://datatracker.ietf.org/doc/draft-ietf-tls-esni/>
-TODO(ECH) update link to RFC.
+=over 4
+
+=item Encrypted ClientHello specification: L<https://datatracker.ietf.org/doc/draft-ietf-tls-esni/>
+
+=item ECH PEM file format specification: L<https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/>
+
+=item RFC8701: L<https://datatracker.ietf.org/doc/html/rfc8701>
+
+=back
+
+ECH command line options are described in the manual pages for
+L<openssl-s_client(1)>, L<openssl-s_server(1)> and L<openssl-ech(1)>.
=head1 HISTORY
-The functionality described here was added in OpenSSL 3.5.
+The functionality described here was added in OpenSSL 4.0.
=head1 COPYRIGHT
-Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
Licensed under the Apache License 2.0 (the "License"). You may not use
this file except in compliance with the License. You can obtain a copy
diff --git a/include/openssl/ech.h b/include/openssl/ech.h
index 24331e6432..ab724ed907 100644
--- a/include/openssl/ech.h
+++ b/include/openssl/ech.h
@@ -9,7 +9,7 @@
/*
* Externally-visible data structures and prototypes for handling
- * Encrypted ClientHello (ECH).
+ * shared-mode Encrypted ClientHello (ECH).
*/
#ifndef OPENSSL_ECH_H
#define OPENSSL_ECH_H
@@ -120,12 +120,6 @@ int SSL_ech_get1_retry_config(SSL *s, unsigned char **ec, size_t *eclen);
*/
int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos,
const size_t protos_len);
-int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx,
- int *decrypted_ok,
- char **inner_sni, char **outer_sni,
- unsigned char *outer_ch, size_t outer_len,
- unsigned char *inner_ch, size_t *inner_len,
- unsigned char **hrrtok, size_t *toklen);
void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f);
int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len);
diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c
index 78390f5cc3..5188dc06ee 100644
--- a/ssl/ech/ech_helper.c
+++ b/ssl/ech/ech_helper.c
@@ -95,7 +95,7 @@ int ossl_ech_helper_get_ch_offsets(const unsigned char *ch, size_t ch_len,
if (!PACKET_get_net_2(&pkt, &pi_tmp))
return 0;
/* if we're not TLSv1.2+ then we can bail, but it's not an error */
- if (pi_tmp != TLS1_2_VERSION && pi_tmp != TLS1_3_VERSION)
+ if (pi_tmp != TLS1_2_VERSION)
return 1;
/* chew up the packet to extensions */
if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE)
diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index f93d92d4d8..579185e70a 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -281,8 +281,7 @@ err:
/*
* Send a random value that looks like a real ECH.
*
- * TODO(ECH): the "best" thing to do here is not yet known. For now, we do
- * GREASEing as currently (20241102) done by chrome:
+ * We do GREASEing as follows:
* - always HKDF-SHA256
* - always AES-128-GCM
* - random config ID, even for requests to same server in same session
@@ -648,9 +647,9 @@ size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
/*
* Finally - make sure final result is longer than padding target
* and a multiple of our padding increment.
- * TODO(ECH): This is a local addition - we might take it out if
- * it makes us stick out; or if we take out the above more (uselessly:-)
- * complicated scheme, we may only need this in the end.
+ * This is a local addition - we might want to take it out if it makes
+ * us stick out; or if we take out the above more (uselessly:-)
+ * complicated scheme above, we may only need this in the end.
*/
if ((length_with_padding % OSSL_ECH_PADDING_INCREMENT) != 0)
length_with_padding += OSSL_ECH_PADDING_INCREMENT
@@ -1091,10 +1090,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
ossl_ech_pbuf("cx: result", acbuf, OSSL_ECH_SIGNAL_LEN);
#endif
/* put confirm value back into transcript */
- if (s->ext.ech.hrrsignal_p == NULL)
- memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN);
- else
- memcpy(conf_loc, s->ext.ech.hrrsignal, OSSL_ECH_SIGNAL_LEN);
+ memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN);
/* on a server, we need to reset the hs buffer now */
if (s->server && s->hello_retry_request == SSL_HRR_NONE)
ossl_ech_reset_hs_buffer(s, s->ext.ech.innerch, s->ext.ech.innerch_len);
@@ -1373,7 +1369,7 @@ static int ech_decode_inbound_ech(SSL_CONNECTION *s, PACKET *pkt,
SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
goto err;
}
- if (pval_tmp > PACKET_remaining(pkt)) {
+ if (pval_tmp == 0 || pval_tmp > PACKET_remaining(pkt)) {
SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
goto err;
}
@@ -1668,7 +1664,7 @@ static int ech_decode_inner(SSL_CONNECTION *s, const unsigned char *ob,
BUF_MEM *di_mem = NULL;
uint16_t outers[OSSL_ECH_OUTERS_MAX]; /* compressed extension types */
size_t n_outers = 0;
- WPACKET di;
+ WPACKET di = { 0 }; /* "fake" pkt for inner */
if (encoded_inner == NULL || ob == NULL || ob_len == 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
@@ -2066,8 +2062,7 @@ int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt)
OPENSSL_free(aad);
aad = NULL;
s->ext.ech.done = 1; /* decrypting worked or not, but we're done now */
- /* 3. if decrypt fails tee-up GREASE */
- s->ext.ech.grease = OSSL_ECH_IS_GREASE;
+ s->ext.ech.grease = OSSL_ECH_IS_GREASE; /* if decrypt fails tee-up GREASE */
s->ext.ech.success = 0;
if (clear != NULL) {
s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
@@ -2103,6 +2098,7 @@ int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt)
goto err;
}
OPENSSL_free(clear);
+ clear = NULL;
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("Inner CH (decoded)", s->ext.ech.innerch,
s->ext.ech.innerch_len);
diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h
index c82869fa0e..0a49f4c77e 100644
--- a/ssl/ech/ech_local.h
+++ b/ssl/ech/ech_local.h
@@ -155,8 +155,13 @@ struct ossl_echstore_st {
/* ECH details associated with an SSL_CTX */
typedef struct ossl_ech_ctx_st {
- /* TODO(ECH): consider making es ref-counted */
- OSSL_ECHSTORE *es;
+ /*
+ * We could make es ref-counted, but that seems like a premature
+ * optimisation, given we don't currently expect many applications
+ * to have many SSL_CTX/SSL structures using many ECH configurations.
+ * Could fairly easily be done if experience warrants.
+ */
+ OSSL_ECHSTORE *es; /* ECHConfigList details */
unsigned char *alpn_outer;
size_t alpn_outer_len;
SSL_ech_cb_func cb; /* callback function for when ECH "done" */
@@ -164,7 +169,12 @@ typedef struct ossl_ech_ctx_st {
/* ECH details associated with an SSL_CONNECTION */
typedef struct ossl_ech_conn_st {
- /* TODO(ECH): consider making es ref-counted */
+ /*
+ * We could make es ref-counted, but that seems like a premature
+ * optimisation, given we don't currently expect many applications
+ * to have many SSL_CTX/SSL structures using many ECH configurations.
+ * Could fairly easily be done if experience warrants.
+ */
OSSL_ECHSTORE *es; /* ECHConfigList details */
int no_outer; /* set to 1 if we should send no outer SNI at all */
char *outer_hostname;
@@ -203,9 +213,11 @@ typedef struct ossl_ech_conn_st {
uint16_t outer_only[OSSL_ECH_OUTERS_MAX];
size_t n_outer_only; /* the number of outer_only extensions so far */
/*
+ * We store/access the index of the extension handler in
+ * s->ext.ech.ext_ind, as we'd otherwise not know it here.
+ * Be nice were there a better way to handle that.
* Index of the current extension's entry in ext_defs - this is
* to avoid the need to change a couple of extension APIs.
- * TODO(ECH): check if there's another way to get that value
*/
int ext_ind;
/* ECH status vars */
diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c
index 2fddcef9a5..d7b7b8e5e7 100644
--- a/ssl/ech/ech_ssl_apis.c
+++ b/ssl/ech/ech_ssl_apis.c
@@ -248,10 +248,16 @@ int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni)
int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite)
{
SSL_CONNECTION *s;
+ OSSL_HPKE_SUITE hpke_suite_in = OSSL_HPKE_SUITE_DEFAULT;
s = SSL_CONNECTION_FROM_SSL(ssl);
if (s == NULL)
return 0;
+ /* check suite makes sense */
+ if (OSSL_HPKE_str2suite(suite, &hpke_suite_in) != 1) {
+ ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+ return 0;
+ }
OPENSSL_free(s->ext.ech.grease_suite);
s->ext.ech.grease_suite = NULL;
if (suite == NULL)
@@ -259,7 +265,6 @@ int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite)
s->ext.ech.grease_suite = OPENSSL_strdup(suite);
if (s->ext.ech.grease_suite == NULL)
return 0;
- s->ext.ech.attempted = 1;
s->ext.ech.grease = OSSL_ECH_IS_GREASE;
return 1;
}
@@ -272,7 +277,6 @@ int SSL_ech_set_grease_type(SSL *ssl, uint16_t type)
if (s == NULL)
return 0;
s->ext.ech.attempted_type = type;
- s->ext.ech.attempted = 1;
s->ext.ech.grease = OSSL_ECH_IS_GREASE;
return 1;
}
@@ -370,27 +374,6 @@ int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx,
return 1;
}
-int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx,
- int *decrypted_ok,
- char **inner_sni, char **outer_sni,
- unsigned char *outer_ch, size_t outer_len,
- unsigned char *inner_ch, size_t *inner_len,
- unsigned char **hrrtok, size_t *toklen)
-{
- if (ctx == NULL) {
- /*
- * TODO(ECH): this is a bit of a bogus error, just so as
- * to get the `make update` command to add the required
- * error number. We don't need it yet, but it's involved
- * in some of the build artefacts, so may as well jump
- * the gun a bit on it.
- */
- ERR_raise(ERR_LIB_SSL, SSL_R_ECH_REQUIRED);
- return 0;
- }
- return 0;
-}
-
void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f)
{
if (ctx == NULL) {
diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c
index 615cb45630..41912f4b44 100644
--- a/ssl/ech/ech_store.c
+++ b/ssl/ech/ech_store.c
@@ -46,9 +46,9 @@ static const char B64_alphabet[] = "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b
#ifndef TLSEXT_MINLEN_host_name
/*
- * TODO(ECH): shortest DNS name we allow, e.g. "a.bc" - maybe that should
- * be defined elsewhere, or should the check be skipped in case there's
- * a local deployment that uses shorter names?
+ * TODO(ECH): Decide this. The shortest DNS name we allow, e.g. "a.bc" - maybe
+ * that should be defined elsewhere, or should the check be skipped in case
+ * there's a local deployment that uses shorter names?
*/
#define TLSEXT_MINLEN_host_name 4
#endif
@@ -106,7 +106,7 @@ void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee)
*
* This is intended for small inputs, either files or buffers and
* not other kinds of BIO.
- * TODO(ECH): how to check for oddball input BIOs?
+ * TODO(ECH): is there really a way to check for oddball input BIOs?
*/
static int ech_bio2buf(BIO *in, unsigned char **buf, size_t *len)
{
@@ -769,6 +769,7 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
ee->max_name_length = max_name_length;
ee->config_id = config_id;
ee->keyshare = privp;
+ privp = NULL; /* don't free twice */
/* "steal" the encoding from the memory */
ee->encoded = (unsigned char *)epkt_mem->data;
ee->encoded_len = bblen;
@@ -926,6 +927,7 @@ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs,
goto err;
}
*has_private = (ee->keyshare == NULL ? 0 : 1);
+ *for_retry = ee->for_retry;
/* Now "print" the ECHConfigList */
out = BIO_new(BIO_s_mem());
if (out == NULL) {
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index 657aeeee2c..9dba2d7446 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -572,7 +572,11 @@ int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt)
#endif
if (s == NULL || s->ext.ech.es == NULL)
return OSSL_ECH_SAME_EXT_CONTINUE; /* nothing to do */
- /* TODO(ECH): we need a better way to handle indexing exts */
+ /*
+ * We store/access the index of the extension handler in
+ * s->ext.ech.ext_ind, as we'd otherwise not know it here.
+ * Be nice were there a better way to handle that.
+ */
tind = s->ext.ech.ext_ind;
/* If this index'd extension won't be compressed, we're done */
if (tind < 0 || tind >= nexts)
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index e323e21cf3..ea2a854a89 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -1430,7 +1430,6 @@ dopsksess:
* with random values of the same length.
*/
if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) {
- /* TODO(ECH): changes here need testing with server-side code PR */
unsigned char *rndbuf = NULL, *rndbufp = NULL;
size_t totalrndsize = 0;
@@ -2577,8 +2576,9 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0;
size_t info_len = OSSL_ECH_MAX_INFO_LEN, clear_len = 0, encoded_len = 0;
/* whether or not we've been asked to GREASE, one way or another */
- int grease_opt_set = ((s->ext.ech.grease == OSSL_ECH_IS_GREASE)
- || ((s->options & SSL_OP_ECH_GREASE) != 0));
+ int grease_opt_set = (s->ext.ech.attempted != 1
+ && ((s->ext.ech.grease == OSSL_ECH_IS_GREASE)
+ || ((s->options & SSL_OP_ECH_GREASE) != 0)));
/* if we're not doing real ECH and not GREASEing then exit */
if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech && grease_opt_set == 0)
@@ -2651,9 +2651,28 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
OSSL_TRACE_END(TLS);
config_id_to_use = ee->config_id; /* if requested, use a random config_id instead */
if ((s->options & SSL_OP_ECH_IGNORE_CID) != 0) {
- if (RAND_bytes_ex(sctx->libctx, &config_id_to_use, 1, 0) <= 0) {
- SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
- return 0;
+ int max_iters = 1000, i = 0;
+
+ /* rejection sample to get a different but random config_id */
+ while (config_id_to_use == ee->config_id) {
+#ifdef OSSL_ECH_SUPERVERBOSE
+ if (i > 0) {
+ OSSL_TRACE_BEGIN(TLS)
+ {
+ BIO_printf(trc_out, "EAAE: rejected random-config %02x\n",
+ config_id_to_use);
+ }
+ OSSL_TRACE_END(TLS);
+ }
+#endif
+ if (RAND_bytes_ex(sctx->libctx, &config_id_to_use, 1, 0) <= 0) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+ if (i++ >= max_iters) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("EAAE: random config_id", &config_id_to_use, 1);
diff --git a/ssl/statem/extensions_cust.c b/ssl/statem/extensions_cust.c
index 69138c1039..157aaa5025 100644
--- a/ssl/statem/extensions_cust.c
+++ b/ssl/statem/extensions_cust.c
@@ -222,7 +222,11 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x,
OSSL_TRACE_END(TLS);
}
if (s->ext.ech.ch_depth == 0) {
- /* TODO(ECH): we need a better way to handle indexing exts */
+ /*
+ * We store/access the index of the extension handler in
+ * s->ext.ech.ext_ind, as we'd otherwise not know it here.
+ * Be nice were there a better way to handle that.
+ */
/* copy over the extension octets (if any) to outer */
int j, tind = -1;
RAW_EXTENSION *raws = NULL;
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index 81bf5d91bd..a953a175ee 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -3391,11 +3391,6 @@ int tls_process_initial_server_flight(SSL_CONNECTION *s)
#ifndef OPENSSL_NO_ECH
/* check result of ech and return error if needed */
- /*
- * TODO(ECH): check that we never get here in a server
- * during split-mode or test cases - there used be a
- * check of !s->server added to the below.
- */
if (s->ext.ech.es != NULL
&& s->ext.ech.attempted == 1
&& s->ext.ech.success != 1
diff --git a/test/build.info b/test/build.info
index fbf0b20caf..23bc75c7ca 100644
--- a/test/build.info
+++ b/test/build.info
@@ -73,6 +73,10 @@ IF[{- !$disabled{tests} -}]
x509_acert_test x509_req_test strtoultest bio_pw_callback_test \
engine_stubs_test base64_simdutf_test bio_eof_test ech_test
+ IF[{- !$disabled{'ech'} -}]
+ PROGRAMS{noinst}=ech_corrupt_test
+ ENDIF
+
IF[{- !$disabled{'rpk'} -}]
PROGRAMS{noinst}=rpktest
ENDIF
@@ -232,6 +236,10 @@ IF[{- !$disabled{tests} -}]
INCLUDE[ech_test]=../include ../apps/include
DEPEND[ech_test]=../libssl.a ../libcrypto.a libtestutil.a
+ SOURCE[ech_corrupt_test]=ech_corrupt_test.c helpers/ssltestlib.c
+ INCLUDE[ech_corrupt_test]=../include ../apps/include
+ DEPEND[ech_corrupt_test]=../libssl.a ../libcrypto.a libtestutil.a
+
SOURCE[evp_extra_test2]=evp_extra_test2.c $INITSRC tls-provider.c
INCLUDE[evp_extra_test2]=../include ../apps/include
DEPEND[evp_extra_test2]=../libcrypto libtestutil.a
diff --git a/test/certs/ech-big.pem b/test/certs/echdir/ech-big.pem
similarity index 100%
rename from test/certs/ech-big.pem
rename to test/certs/echdir/ech-big.pem
diff --git a/test/certs/ech-eg.pem b/test/certs/echdir/ech-eg.pem
similarity index 100%
rename from test/certs/ech-eg.pem
rename to test/certs/echdir/ech-eg.pem
diff --git a/test/certs/ech-giant.pem b/test/certs/echdir/ech-giant.pem
similarity index 100%
rename from test/certs/ech-giant.pem
rename to test/certs/echdir/ech-giant.pem
diff --git a/test/certs/ech-mid.pem b/test/certs/echdir/ech-mid.pem
similarity index 100%
rename from test/certs/ech-mid.pem
rename to test/certs/echdir/ech-mid.pem
diff --git a/test/certs/ech-rsa.pem b/test/certs/echdir/ech-rsa.pem
similarity index 100%
rename from test/certs/ech-rsa.pem
rename to test/certs/echdir/ech-rsa.pem
diff --git a/test/certs/echdir/echconfig-10.pem b/test/certs/echdir/echconfig-10.pem
new file mode 100644
index 0000000000..d1d218f9cd
--- /dev/null
+++ b/test/certs/echdir/echconfig-10.pem
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VuBCIEIPiqiukxstIS9ViT4zWlewgPbjN4XVQ1XrcYgxwE6sl1
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+AED+CgA8vwAgACCB9YyilgR2NMLVPOsESVceIrfGpXThGIUMIwGfGClmSgAEAAEAAQAAAAtleGFtcGxlLmNvbQAA
+-----END ECHCONFIG-----
diff --git a/test/certs/echdir/echconfig-256.pem b/test/certs/echdir/echconfig-256.pem
new file mode 100644
index 0000000000..44228fa929
--- /dev/null
+++ b/test/certs/echdir/echconfig-256.pem
@@ -0,0 +1,8 @@
+-----BEGIN PRIVATE KEY-----
+MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfVyqIGiyInH7KDB3
+szOuOFYP41zV2gpA8wESUjpcqrChRANCAAQaLs1qXa+t31TtQeD6hXwWoltwJgL1
+zt9kMsrj1HMScUQ8yIpNt+KUjzuMLbJxG9SpuZqfFW/agXJlyQEmatGE
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+AGH+CgBd0QAQAEEEGi7Nal2vrd9U7UHg+oV8FqJbcCYC9c7fZDLK49RzEnFEPMiKTbfilI87jC2ycRvUqbmanxVv2oFyZckBJmrRhAAEAAEAAQAAAAtleGFtcGxlLmNvbQAA
+-----END ECHCONFIG-----
diff --git a/test/certs/echdir/echconfig_bad_fuzz1.pem b/test/certs/echdir/echconfig_bad_fuzz1.pem
new file mode 100644
index 0000000000..5174028c32
--- /dev/null
+++ b/test/certs/echdir/echconfig_bad_fuzz1.pem
@@ -0,0 +1 @@
+AD7+DQA6uAAgACAogff+HZbirYdQCfXI01GBPP8AEKYyK/D/0DoeXD84fgAQAAEAAQgLZXhhbUNwbGUuYwYAAAAAQwA=
diff --git a/test/certs/echdir/echmaxname.pem b/test/certs/echdir/echmaxname.pem
new file mode 100644
index 0000000000..c1147e1cde
--- /dev/null
+++ b/test/certs/echdir/echmaxname.pem
@@ -0,0 +1,7 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VuBCIEILCwRG3jPpLC4JRINol54Jo8bvua3e1EV3EDbepDCmpi
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+AD7+DQA6IwAgACDT+hqR+Atl9LejS6nJrr9oDspGvwWTGOW3WJHdJy+OSQAEAAEA
+ASALZXhhbXBsZS5jb20AAA==
+-----END ECHCONFIG-----
diff --git a/test/certs/echdir/echwithexts.pem b/test/certs/echdir/echwithexts.pem
new file mode 100644
index 0000000000..921bb0717f
--- /dev/null
+++ b/test/certs/echdir/echwithexts.pem
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VuBCIEIOAOgc22M+RJc5QZdY/ndZSME8akKHon13JITeB899pF
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+Ah3+DQIZAAAgACAgiyA/dGsDu4MO8Qn4xriSBhRxhNmfQ5jtBGJg1o5VbQAEAAEAAQALZXhhbXBsZS5jb20B3//KAAD/ywAMaGVsbG8gd29ybGQK/8wBx4lQTkcNChoKAAAADUlIRFIAAAAKAAAABggDAAAAzS7/9AAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAGxQTFRFAwMFAwQHAgMFAgEBDQgIDwkKDQcIEAkJAAABAgICAAECBgUJBgQEAQICAAEBAAAABgUFAgIDCQYHCgcIBgcMAgIEAQEBAQECCAUGBAMDBQMDCAcMBAQHDQgJAgMDBwcMAQIDBQYIAQED////go9xWgAAAAFiS0dEIypibDoAAAAJcEhZcwAAASwAAAEsAHOI6VIAAAAHdElNRQfnBAQMGAw/wJ0bAAAAQ0lEQVQI1wXBiQJAIBQEwJUOuVbo5Qjx/x9pBqhUrY11toFvO9MPI92EMC9rpKRtx6FFndknIS7ejE8hCRFJ4c2f8AdkNAPhOzEsxgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMy0wNC0wNFQxMjoxMTo1OSswMDowMAbaIaMAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjMtMDQtMDRUMTI6MTE6NTkrMDA6MDB3h5kfAAAAAElFTkSuQmCC
+-----END ECHCONFIG-----
diff --git a/test/ech_corrupt_test.c b/test/ech_corrupt_test.c
new file mode 100644
index 0000000000..2c68d49c4c
--- /dev/null
+++ b/test/ech_corrupt_test.c
@@ -0,0 +1,1691 @@
+/*
+ * Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <string.h>
+#include "helpers/ssltestlib.h"
+#include "testutil.h"
+#include <openssl/ech.h>
+#include <internal/ech_helpers.h>
+#include <internal/packet.h>
+
+#define OSSL_ECH_MAX_LINELEN 1000 /* for a sanity check */
+#define DEF_CERTS_DIR "test/certs"
+
+/* the testcase numbers */
+#define TESTCASE_CH 1
+#define TESTCASE_SH 2
+#define TESTCASE_ECH 3
+
+static OSSL_LIB_CTX *libctx = NULL;
+static char *propq = NULL;
+static OSSL_ECHSTORE *es = NULL;
+static OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+static int verbose = 0;
+static int testcase = 0;
+static int testiter = 0;
+static char *certsdir = NULL;
+static char *cert = NULL;
+static char *privkey = NULL;
+static unsigned char *hpke_info = NULL;
+static size_t hpke_infolen = 0;
+static int short_test = 0;
+
+/*
+ * An x25519 ech key and ECHConfigList with public name example.com
+ * and the associated base64 encoded and binary forms of that
+ * ECHConfigList - hardcoding here is ok as we're testing for
+ * effects of corrupted CH/SH and not for ECHConfig badness.
+ */
+static const char pem_kp1[] = "-----BEGIN PRIVATE KEY-----\n"
+ "MC4CAQAwBQYDK2VuBCIEILDIeo9Eqc4K9/uQ0PNAyMaP60qrxiSHT2tNZL3ksIZS\n"
+ "-----END PRIVATE KEY-----\n"
+ "-----BEGIN ECHCONFIG-----\n"
+ "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
+ "AQALZXhhbXBsZS5jb20AAA==\n"
+ "-----END ECHCONFIG-----\n";
+static const char echconfig[] = "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA"
+ "AQALZXhhbXBsZS5jb20AAA==";
+static size_t echconfiglen = sizeof(echconfig) - 1;
+static unsigned char bin_echconfig[] = {
+ 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0x6c, 0x00,
+ 0x20, 0x00, 0x20, 0x98, 0xec, 0x1d, 0x1f, 0xff,
+ 0x72, 0xaf, 0x1c, 0x81, 0x5d, 0xa2, 0xa1, 0x5a,
+ 0x39, 0xb7, 0x54, 0xf1, 0x86, 0x14, 0xf8, 0xc7,
+ 0x41, 0x7f, 0x8b, 0xf3, 0x6c, 0xb8, 0x40, 0x00,
+ 0xbd, 0x90, 0x0b, 0x00, 0x04, 0x00, 0x01, 0x00,
+ 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+ 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+static size_t bin_echconfiglen = sizeof(bin_echconfig);
+
+/*
+ * We can grab the CH and SH and manipulate those to check good
+ * behaviour in the face of various errors. The most important
+ * thing to test is the server processing of the new combinations
+ * that result from the EncodedInnerClientHello (basically the raw
+ * output of ECH decryption). We test that via test vectors for
+ * those various borked values that we encrypt (via HPKE) and
+ * inject into the CH. The SH is much simpler since there are
+ * far fewer things to test with the magic encoding of the ECH
+ * accept signal into the SH.random or HRR.extension, but we
+ * can also test with borked versions of those.
+ *
+ * We'd like to, but so far cannot, do similarly for the ECH
+ * retry-config in EncryptedExtensions. Seems like there's no
+ * good way to get at the plaintext there and replace it with
+ * a borked value. (QUIC tests seem to have a way to do that
+ * but I've yet to figure how to replicate that here for the
+ * retry-config.)
+ */
+
+/*
+ * For client hello, we use a set of test vectors for each test:
+ * - encoded inner CH prefix
+ * - encoded inner CH for borking (esp. outer extensions)
+ * - encoded inner CH postfix
+ * - expected result (1 for good, 0 for bad)
+ * - expected error reason in the case of bad
+ *
+ * For each test, we replace the ECH ciphertext with a value
+ * that's the HPKE seal/enc of an encoded inner-CH made up of
+ * the three parts above and then see if we get the expected
+ * error (reason).
+ *
+ * Whenever we re-seal we will get an error due to using the
+ * wrong inner client random, which we don't know. But that
+ * differs from errors in handling decoding after decryption.
+ *
+ * The inner CH is split in 3 variables so we can re-use pre
+ * and post values, making it easier to understand/manipulate
+ * a corrupted-or-not value.
+ *
+ * Note that the overall length of the encoded inner needs to
+ * be maintained as otherwise outer length fields that are not
+ * re-computed will be wrong. (We include a test of that as
+ * well.) A radical change in the content of encoded inner
+ * values (e.g. eliminating compression entirely) could break
+ * these tests, but minor changes should have no effect due to
+ * padding. (Such a radical change showing up as a fail of
+ * these tests is arguably a good outcome.)
+ */
+typedef struct {
+ const unsigned char *pre;
+ size_t prelen;
+ const unsigned char *forbork;
+ size_t fblen;
+ const unsigned char *post;
+ size_t postlen;
+ int rv_expected; /* expected result */
+ int err_expected; /* expected error */
+} TEST_ECHINNER;
+
+/* a full padded, encoded inner client hello */
+static const unsigned char entire_encoded_inner[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x34, 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33,
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* a full padded, encoded inner client hello with no extensions */
+static const unsigned char no_ext_encoded_inner[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* a too-short, encoded inner client hello */
+static const unsigned char outer_short_encoded_inner[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x34, 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33,
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01
+};
+
+/* inner prefix up as far as outer_exts */
+static const unsigned char encoded_inner_pre[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x34
+};
+
+/* inner prefix with mad length of suites (0xDDDD) */
+static const unsigned char badsuites_inner_pre[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0xDD, 0xDD, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x34
+};
+
+/* outer extensions - we play with variations of this */
+static const unsigned char encoded_inner_outers[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* outers with repetition of one extension (0x0B) */
+static const unsigned char borked_outer1[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0B, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* outers including a non-used extension (0xFFAB) */
+static const unsigned char borked_outer2[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0xFF, 0xAB, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* refer to SNI in outers! 2nd-last is 0x0000 */
+static const unsigned char borked_outer3[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x00, 0x00, 0x33
+};
+
+/* refer to ECH (0xfe0d) within outers */
+static const unsigned char borked_outer4[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0xFE, 0x0D, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* refer to outers (0xfd00) within outers */
+static const unsigned char borked_outer5[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0xFD, 0x00, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* no outers at all! include unknown ext 0xFF99 instead */
+static const unsigned char borked_outer6[] = {
+ 0xFF, 0x99, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/*
+ * outer with bad length (even number of octets)
+ * we add a short bogus extension (0xFFFF) after
+ * to ensure overall decode succeeds
+ */
+static const unsigned char borked_outer7[] = {
+ 0xfd, 0x00, 0x00, 0x0E, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0xFF, 0xFF, 0x00, 0x01, 0x00
+};
+
+/* outer with bad inner length (odd number of octets) */
+static const unsigned char borked_outer8[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x11, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* outer with HUGE length (0xFF13) */
+static const unsigned char borked_outer9[] = {
+ 0xfd, 0x00, 0xFF, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* outer with zero length, followed by bogus ext */
+static const unsigned char borked_outer10[] = {
+ 0xfd, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00,
+ 0x0F, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* refer to key-share 0x00 0x33 (51) twice within outers */
+static const unsigned char borked_outer11[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x33, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+/* refer to psk kex mode (0x00 0x2D/45) within outers */
+static const unsigned char borked_outer12[] = {
+ 0xfd, 0x00, 0x00, 0x13, 0x12, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x2D, 0x00, 0x2b, 0x00, 0x2d, 0x00, 0x33
+};
+
+static const unsigned char encoded_inner_post[] = {
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* muck up the padding by including non-zero stuff */
+static const unsigned char bad_pad_encoded_inner_post[] = {
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* an encoded inner that's just too short */
+static const unsigned char short_encoded_inner[] = {
+ 0x03, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/*
+ * too many outer extensions - max is 20 (decimal)
+ * defined as OSSL_ECH_OUTERS_MAX
+ */
+static const unsigned char too_many_outers[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00,
+ 0x00, 0x4c, /* extslen, incl. our added outers */
+ 0xfd, 0x00, /* outers */
+ 0x00, 0x2b, /* len of outers */
+ 0x2a, /* above minus one (42) 21 outers */
+ 0x00, 0x0b, /* the 9 'normal' outers */
+ 0x00, 0x0a,
+ 0x00, 0x23,
+ 0x00, 0x16,
+ 0x00, 0x17,
+ 0x00, 0x0d,
+ 0x00, 0x2b,
+ 0x00, 0x2d,
+ 0x00, 0x33,
+ /* 12 more outers, set 'em all to ALPN (16, 0x10) */
+ 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10,
+ 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10,
+ 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10,
+ /* and now the inner SNI, inner ECH and 3 padding octets */
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01,
+ 0x00, 0x00, 0x00
+};
+
+/*
+ * a full padded, encoded inner client hello, but
+ * without an inner supported extensions, (take
+ * out the 0x00 0x2b and add some padding zeros,
+ * adjusting lengths) and hence meaning TLSv1.2
+ */
+static const unsigned char no_supported_exts[] = {
+ 0x03, 0x03, 0x7b, 0xe8, 0xc1, 0x18, 0xd7, 0xd1,
+ 0x9c, 0x39, 0xa4, 0xfa, 0xce, 0x75, 0x72, 0x40,
+ 0xcf, 0x37, 0xbb, 0x4c, 0xcd, 0xa7, 0x62, 0xda,
+ 0x04, 0xd2, 0xdb, 0xe2, 0x89, 0x33, 0x36, 0x15,
+ 0x96, 0xc9, 0x00, 0x00, 0x08, 0x13, 0x02, 0x13,
+ 0x03, 0x13, 0x01, 0x00, 0xff, 0x01, 0x00, 0x00,
+ 0x32, 0xfd, 0x00, 0x00, 0x11, 0x10, 0x00, 0x0b,
+ 0x00, 0x0a, 0x00, 0x23, 0x00, 0x16, 0x00, 0x17,
+ 0x00, 0x0d, 0x00, 0x2d, 0x00, 0x33,
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00
+};
+
+static const unsigned char tlsv12_inner[] = {
+ 0x03, 0x03, /* version, then client-random */
+ 0x23, 0xc3, 0xa0, 0x49, 0xea, 0x17, 0x9e, 0x30,
+ 0x6f, 0x0e, 0xc9, 0x79, 0xd0, 0xd1, 0xfd, 0xea,
+ 0x63, 0xfd, 0x20, 0x04, 0xaa, 0xb3, 0x2a, 0x29,
+ 0xf5, 0x96, 0x60, 0x29, 0x42, 0x7e, 0x5c, 0x7b,
+ 0x00, /* zero'd session ID */
+ 0x00, 0x02, /* ciphersuite len, just one */
+ 0xc0, 0x2c, /* a TLSv1.2 ciphersuite */
+ 0x01, 0x00, /* no compression */
+ 0x00, 0x32, /* extslen */
+ 0xfd, 0x00, /* outers */
+ 0x00, 0x11, /* len of outers */
+ 0x10, /* above minus one (16) 8 outers */
+ 0x00, 0x0b, /* the 'normal' outers, minus supported_versions */
+ 0x00, 0x0a,
+ 0x00, 0x23,
+ 0x00, 0x16,
+ 0x00, 0x17,
+ 0x00, 0x0d,
+ 0x00, 0x2d,
+ 0x00, 0x33,
+ /* and now the inner SNI, inner ECH and padding octets */
+ 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, 0x00,
+ 0x0f, 0x66, 0x6f, 0x6f, 0x2e, 0x65, 0x78, 0x61,
+ 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
+ 0xfe, 0x0d, 0x00, 0x01, 0x01,
+ 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+/* A set of test vectors */
+static TEST_ECHINNER test_inners[] = {
+ /* 1. basic case - copy to show test code works with no change */
+ { NULL, 0, NULL, 0, NULL, 0, 1, SSL_ERROR_NONE },
+
+ /* 2. too-short encoded inner */
+ { NULL, 0,
+ outer_short_encoded_inner, sizeof(outer_short_encoded_inner),
+ NULL, 0,
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 3. otherwise-correct case that fails only due to client random */
+ { NULL, 0,
+ entire_encoded_inner, sizeof(entire_encoded_inner),
+ NULL, 0,
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 4. otherwise-correct case that fails only due to client random */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ encoded_inner_outers, sizeof(encoded_inner_outers),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 5. fails HPKE decryption due to bad padding so treated as GREASE */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ encoded_inner_outers, sizeof(encoded_inner_outers),
+ bad_pad_encoded_inner_post, sizeof(bad_pad_encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC },
+
+ /*
+ * 6. unsupported extension instead of outers - resulting decoded
+ * inner missing so much it seems to be the wrong protocol
+ */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer6, sizeof(borked_outer6),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result - error is different with -notls1_2 */
+#ifdef OPENSSL_NO_TLS1_2
+ SSL_R_VERSION_TOO_LOW
+#else
+ SSL_R_UNSUPPORTED_PROTOCOL
+#endif
+ },
+
+ /* 7. madly long ciphersuites in inner */
+ { badsuites_inner_pre, sizeof(badsuites_inner_pre),
+ encoded_inner_outers, sizeof(encoded_inner_outers),
+ encoded_inner_post, sizeof(bad_pad_encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_TLSV1_ALERT_DECODE_ERROR },
+ /* 8. so many padding bytes recovered clear is short */
+ { NULL, 0,
+ short_encoded_inner, sizeof(short_encoded_inner),
+ NULL, 0,
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+
+ /* 9. repeated codepoint inside outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer1, sizeof(borked_outer1),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 10. non-existent codepoint inside outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer2, sizeof(borked_outer2),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 11. include SNI in outers as well as both inner and outer */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer3, sizeof(borked_outer3),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 12. refer to ECH within outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer4, sizeof(borked_outer4),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 13. refer to outers within outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer5, sizeof(borked_outer5),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 14. bad length of outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer7, sizeof(borked_outer7),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 15. bad inner length in outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer8, sizeof(borked_outer8),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 16. HUGE length in outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer9, sizeof(borked_outer9),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 17. zero length in outers */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer10, sizeof(borked_outer10),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 18. case with no extensions at all */
+ { NULL, 0,
+ no_ext_encoded_inner, sizeof(no_ext_encoded_inner),
+ NULL, 0,
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /*
+ * 19. include key-share twice in outers as well as both inner and outer.
+ * There was a change with this one recently that can/does cause a
+ * different error message (used to be SSL_R_BAD_EXTENSION, but now
+ * mostly ERR_R_INTERNAL_ERROR). The issue is that this test repeats the
+ * key_share in the compressed exts and with PQ kybrid KEMs those are
+ * so large that instead of detecting the duplicate extension we see
+ * an earlier error where the inner CH is bigger than the outer.
+ */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer11, sizeof(borked_outer11),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+#ifdef OPENSSL_NO_ML_KEM
+ SSL_R_BAD_EXTENSION
+#else
+ ERR_R_INTERNAL_ERROR
+#endif
+ },
+ /* 20. include psk key mode ext in outers as well as both inner and outer */
+ { encoded_inner_pre, sizeof(encoded_inner_pre),
+ borked_outer12, sizeof(borked_outer12),
+ encoded_inner_post, sizeof(encoded_inner_post),
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /* 21. too many outers */
+ { NULL, 0,
+ too_many_outers, sizeof(too_many_outers),
+ NULL, 0,
+ 0, /* expected result */
+ SSL_R_BAD_EXTENSION },
+ /*
+ * 22. no supported_versions hence TLSv1.2, with server set to
+ * allow max tlsv1.3
+ */
+ { NULL, 0,
+ no_supported_exts, sizeof(no_supported_exts),
+ NULL, 0,
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /*
+ * 23. no supported_versions hence TLSv1.2, with server set to
+ * allow max tlsv1.2
+ */
+ { NULL, 0,
+ no_supported_exts, sizeof(no_supported_exts),
+ NULL, 0,
+ 0, /* expected result */
+ SSL_R_NO_PROTOCOLS_AVAILABLE },
+ /*
+ * 24. no supported_versions hence TLSv1.2, with server set to
+ * allow min tlsv1.2
+ */
+ { NULL, 0,
+ no_supported_exts, sizeof(no_supported_exts),
+ NULL, 0,
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 25. smuggled TLSv1.2 CH */
+ { NULL, 0,
+ tlsv12_inner, sizeof(tlsv12_inner),
+ NULL, 0,
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+
+};
+
+/*
+ * For server hello/HRR, we use a set of test vectors for each test:
+ *
+ * - borkage encodes what we're breaking and is the OR
+ * of some #define'd OSSL_ECH_BORK_* flags
+ * - bork is the value to use instead of the real one (or NULL)
+ * - blen is the size of bork
+ * - rv_expected is the return value expected for the connection
+ * - err_expected is the reason code we expect to see
+ */
+typedef struct {
+ int borkage; /* type of borkage */
+ unsigned char *bork; /* borked value */
+ size_t blen; /* len(bork) */
+ int rv_expected; /* expected result */
+ int err_expected; /* expected error */
+} TEST_SH;
+
+#define OSSL_ECH_BORK_NONE 0
+#define OSSL_ECH_BORK_FLIP 1
+#define OSSL_ECH_BORK_HRR (1 << 1)
+#define OSSL_ECH_BORK_SHORT_HRR_CONFIRM (1 << 2)
+#define OSSL_ECH_BORK_LONG_HRR_CONFIRM (1 << 3)
+#define OSSL_ECH_BORK_GREASE (1 << 4)
+#define OSSL_ECH_BORK_REPLACE (1 << 5)
+
+/* a truncated ECH, with another bogus ext to match overall length */
+static unsigned char shortech[] = {
+ 0xfe, 0x0d, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
+ 0xdd, 0xdd, 0x00, 0x00
+};
+
+/* a too-long ECH internal length */
+static unsigned char longech[] = {
+ 0xfe, 0x0d, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00,
+ 0xdd, 0xdd, 0x00, 0x00
+};
+
+static TEST_SH test_shs[] = {
+ /* 1. no messing about, should succeed */
+ { OSSL_ECH_BORK_NONE, NULL, 0, 1, SSL_ERROR_NONE },
+ /* 2. trigger HRR but no other borkage */
+ { OSSL_ECH_BORK_HRR, NULL, 0, 1, SSL_ERROR_NONE },
+
+ /* 3. GREASE and trigger HRR */
+ { OSSL_ECH_BORK_HRR | OSSL_ECH_BORK_GREASE,
+ NULL, 0, 1, SSL_ERROR_NONE },
+
+ /* 4. flip bits in SH.random ECH confirmation value */
+ { OSSL_ECH_BORK_FLIP, NULL, 0, 0,
+ SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC },
+ /* 5. flip bits in HRR.exts ECH confirmation value */
+ { OSSL_ECH_BORK_HRR | OSSL_ECH_BORK_FLIP,
+ NULL, 0, 0,
+ SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC },
+ /* 6. truncate HRR.exts ECH confirmation value */
+ { OSSL_ECH_BORK_HRR | OSSL_ECH_BORK_REPLACE,
+ shortech, sizeof(shortech), 0, SSL_R_LENGTH_MISMATCH },
+ /* 7. too-long HRR.exts ECH confirmation value */
+ { OSSL_ECH_BORK_HRR | OSSL_ECH_BORK_REPLACE,
+ longech, sizeof(longech), 0, SSL_R_BAD_EXTENSION },
+
+};
+
+/*
+ * Test vectors for badly encoded ECH extension values for
+ * the outer ClientHelllo. We grab the outbound ClientHello
+ * and overwrite these values in the appropriate place. That
+ * will always break the TLS connection, even with a correct
+ * encoding, as we're breaking the transcript, but we expect
+ * decoding to catch these and to get 'bad extension' errors
+ * in most cases.
+ *
+ * Note that the code for these tests could be more terse as
+ * declaring a separate buffer for each bad value is quite
+ * repetitive, but doing it this way is more readable and more
+ * easily varied/extended.
+ */
+
+/* an entire correctly encoded ECH (len = 190) */
+static unsigned char entire_encoded_ech[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00, /* outer ECH */
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c, /* config id */
+ 0x00, 0x20, /* encap len then encap val */
+ 0x59, 0x87, 0xbe, 0x13, 0xd0, 0xf1, 0x0e, 0x23,
+ 0xcb, 0x28, 0x26, 0xc2, 0x88, 0xd0, 0x8f, 0xac,
+ 0x04, 0x99, 0x54, 0x30, 0xa2, 0x0f, 0xfe, 0x53,
+ 0xf5, 0xa5, 0x92, 0x01, 0xb1, 0x56, 0xd2, 0x3f,
+ 0x00, 0x90, /* payload len then payload */
+ 0x9e, 0xe6, 0xed, 0x1d, 0xe2, 0xef, 0x30, 0xb0,
+ 0x91, 0x00, 0xdc, 0x90, 0x21, 0x9e, 0x5e, 0x6f,
+ 0xcb, 0xb9, 0xb3, 0x05, 0xdd, 0xac, 0x97, 0x71,
+ 0xf0, 0x2d, 0x48, 0xf7, 0x01, 0xf4, 0x68, 0x0c,
+ 0xb4, 0xbe, 0x78, 0x3c, 0xa3, 0xcb, 0x6a, 0x16,
+ 0x7a, 0xfc, 0x33, 0xcd, 0x12, 0xf3, 0x00, 0x2f,
+ 0x3e, 0xaa, 0xef, 0x7c, 0x26, 0xd3, 0x6f, 0x46,
+ 0x8e, 0xb8, 0x54, 0x4c, 0x6a, 0xc3, 0x85, 0x92,
+ 0x44, 0xc1, 0xe2, 0x03, 0xfe, 0xfc, 0xca, 0xff,
+ 0x3b, 0x03, 0x9a, 0xf0, 0xd8, 0xe7, 0x2d, 0xb0,
+ 0xe3, 0x64, 0x9f, 0xb9, 0x78, 0xd3, 0xca, 0x4c,
+ 0xa2, 0xdd, 0x1f, 0x68, 0x9a, 0x9b, 0xcc, 0xb9,
+ 0x79, 0x59, 0xb4, 0xac, 0x4e, 0x7d, 0xce, 0xa3,
+ 0xc7, 0x23, 0xe6, 0x1c, 0xcd, 0x8d, 0xaa, 0xaa,
+ 0xdb, 0x21, 0xa1, 0xec, 0xb8, 0xbe, 0x53, 0x60,
+ 0x4f, 0xf4, 0x0b, 0xef, 0xad, 0x1d, 0x45, 0x62,
+ 0x65, 0x88, 0xfe, 0x15, 0x47, 0x25, 0x61, 0xa5,
+ 0x65, 0x7a, 0x17, 0xaa, 0x08, 0x3f, 0xe8, 0xf2
+};
+
+/* overall length too much */
+static unsigned char too_long_ech[] = {
+ 0xfe, 0x0d, 0xFF, 0xba /* ext type & length */
+};
+
+/* overall length too short */
+static unsigned char too_short_ech[] = {
+ 0xfe, 0x0d, 0x00, 0x00 /* ext type & length */
+};
+
+/* no inner/outer value */
+static unsigned char no_innerouter_ech[] = {
+ 0xfe, 0x0d, 0x00, 0x00, /* ext type & length */
+ 0x00
+};
+
+/* ECH inner/outer bad value */
+static unsigned char bad_innerouter_ech[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0xFF
+};
+
+/* too short to get to KDF */
+static unsigned char too_short_kdf[] = {
+ 0xfe, 0x0d, 0x00, 0x02, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01 /* cipher suite KDF, AEAD */
+};
+
+/* too short to get to AEAD */
+static unsigned char too_short_aead[] = {
+ 0xfe, 0x0d, 0x00, 0x04, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01 /* cipher suite KDF, AEAD */
+};
+
+/* too short to get to config_id */
+static unsigned char too_short_cid[] = {
+ 0xfe, 0x0d, 0x00, 0x05, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c
+};
+
+/* zero length encap (only ok in HRR) */
+static unsigned char zero_encap_len[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00, 0x00
+};
+
+/* too short to get to encap_len */
+static unsigned char too_short_encap_len[] = {
+ 0xfe, 0x0d, 0x00, 0x07, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00
+};
+
+/* too long encap len */
+static unsigned char too_long_encap_len[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0xFF, 0xFF
+};
+
+/* bit long encap len (more than extension) */
+static unsigned char bit_long_encap_len[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00, 0xFF
+};
+
+/* too short to get to payload_len */
+static unsigned char too_short_payload_len[] = {
+ 0xfe, 0x0d, 0x00, 0x29, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00, 0x20, /* encap len then encap val */
+ 0x59, 0x87, 0xbe, 0x13, 0xd0, 0xf1, 0x0e, 0x23,
+ 0xcb, 0x28, 0x26, 0xc2, 0x88, 0xd0, 0x8f, 0xac,
+ 0x04, 0x99, 0x54, 0x30, 0xa2, 0x0f, 0xfe, 0x53,
+ 0xf5, 0xa5, 0x92, 0x01, 0xb1, 0x56, 0xd2, 0x3f,
+ 0x00, 0x90 /* payload len then payload */
+};
+
+/* bit long payload_len */
+static unsigned char bit_long_payload_len[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00, 0x20, /* encap len then encap val */
+ 0x59, 0x87, 0xbe, 0x13, 0xd0, 0xf1, 0x0e, 0x23,
+ 0xcb, 0x28, 0x26, 0xc2, 0x88, 0xd0, 0x8f, 0xac,
+ 0x04, 0x99, 0x54, 0x30, 0xa2, 0x0f, 0xfe, 0x53,
+ 0xf5, 0xa5, 0x92, 0x01, 0xb1, 0x56, 0xd2, 0x3f,
+ 0x00, 0xba /* payload len then payload */
+};
+
+/* zero payload_len */
+static unsigned char zero_payload_len[] = {
+ 0xfe, 0x0d, 0x00, 0xba, /* ext type & length */
+ 0x00,
+ 0x00, 0x01, 0x00, 0x01, /* cipher suite KDF, AEAD */
+ 0x7c,
+ 0x00, 0x20, /* encap len then encap val */
+ 0x59, 0x87, 0xbe, 0x13, 0xd0, 0xf1, 0x0e, 0x23,
+ 0xcb, 0x28, 0x26, 0xc2, 0x88, 0xd0, 0x8f, 0xac,
+ 0x04, 0x99, 0x54, 0x30, 0xa2, 0x0f, 0xfe, 0x53,
+ 0xf5, 0xa5, 0x92, 0x01, 0xb1, 0x56, 0xd2, 0x3f,
+ 0x00, 0x00 /* payload len then payload */
+};
+
+/*
+ * Structure for test vectors for ECH in the outer CH
+ * - value to use to overwrite encoded ECH
+ * - expected result (1 for good, 0 for bad)
+ * - expected error reason in the case of bad
+ *
+ * For each test, we replace the first |len| octets of the
+ * ECH extension in the outer CH with the associated |val|.
+ *
+ * Note that the overall length of the outer CH needs to
+ * be maintained as otherwise outer length fields that are not
+ * re-computed will be wrong. (We include a test of that as
+ * well.) A radical change in the content of encoded inner
+ * values (e.g. eliminating compression entirely) could break
+ * these tests, but minor changes should have no effect due to
+ * padding. (Such a radical change showing up as a fail of
+ * these tests is arguably a good outcome.)
+ */
+typedef struct {
+ const unsigned char *val;
+ size_t len;
+ int rv_expected; /* expected result */
+ int err_expected; /* expected error */
+} TEST_ECHOUTER;
+
+static TEST_ECHOUTER test_echs[] = {
+ /* 1. basic case - copy to show test code works with no change */
+ { NULL, 0, 1, SSL_ERROR_NONE },
+
+ /* 2. good encoding/length but breaks TLS session integrity */
+ { entire_encoded_ech, sizeof(entire_encoded_ech),
+ 0, /* expected result */
+ SSL_R_DECRYPTION_FAILED_OR_BAD_RECORD_MAC },
+ /* 3. ECH length too long */
+ { too_long_ech, sizeof(too_long_ech),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 4. ECH length too short */
+ { too_short_ech, sizeof(too_short_ech),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 5. no inner/outer value */
+ { no_innerouter_ech, sizeof(no_innerouter_ech),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 6. inner/outer bad value */
+ { bad_innerouter_ech, sizeof(bad_innerouter_ech),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 7. too_short_kdf value */
+ { too_short_kdf, sizeof(too_short_kdf),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 8. too_short_aead value */
+ { too_short_aead, sizeof(too_short_aead),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 9. too_short_cid value */
+ { too_short_cid, sizeof(too_short_cid),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 10. zero_encap_len value */
+ { zero_encap_len, sizeof(zero_encap_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 11. too_short_encap_len value */
+ { too_short_encap_len, sizeof(too_short_encap_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 12. too_long_encap_len value */
+ { too_long_encap_len, sizeof(too_long_encap_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 13. bit_long_encap_len value */
+ { bit_long_encap_len, sizeof(bit_long_encap_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 14. too_short_payload_len value */
+ { too_short_payload_len, sizeof(too_short_payload_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 15. bit_long_payload_len value */
+ { bit_long_payload_len, sizeof(bit_long_payload_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+ /* 16. zero_payload_len value */
+ { zero_payload_len, sizeof(zero_payload_len),
+ 0, /* expected result */ SSL_R_BAD_EXTENSION },
+};
+
+/*
+ * Given a SH (or HRR) find the offsets of the ECH (if any)
+ * sh is the SH buffer
+ * sh_len is the length of the SH
+ * exts points to offset of extensions
+ * echoffset points to offset of ECH
+ * echtype points to the ext type of the ECH
+ * for success, other otherwise
+ *
+ * Offsets are returned to the type or length field in question.
+ * Offsets are set to zero if relevant thing not found.
+ *
+ * Note: input here is untrusted!
+ */
+static int ech_get_sh_offsets(const unsigned char *sh,
+ size_t sh_len, size_t *exts,
+ size_t *echoffset, uint16_t *echtype)
+{
+ unsigned int elen = 0, etype = 0, pi_tmp = 0;
+ const unsigned char *pp_tmp = NULL, *shstart = NULL, *estart = NULL;
+ PACKET pkt;
+ size_t extlens = 0;
+ int done = 0;
+#ifdef OSSL_ECH_SUPERVERBOSE
+ size_t echlen = 0; /* length of ECH, including type & ECH-internal length */
+ size_t sessid_offset = 0;
+ size_t sessid_len = 0;
+#endif
+
+ if (sh == NULL || sh_len == 0 || exts == NULL || echoffset == NULL
+ || echtype == NULL)
+ return 0;
+ *exts = *echoffset = *echtype = 0;
+ if (!PACKET_buf_init(&pkt, sh, sh_len))
+ return 0;
+ shstart = PACKET_data(&pkt);
+ if (!PACKET_get_net_2(&pkt, &pi_tmp))
+ return 0;
+ /* if we're not TLSv1.2+ then we can bail, but it's not an error */
+ if (pi_tmp != TLS1_2_VERSION)
+ return 1;
+ if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE)
+#ifdef OSSL_ECH_SUPERVERBOSE
+ || (sessid_offset = PACKET_data(&pkt) - shstart) == 0
+#endif
+ || !PACKET_get_1(&pkt, &pi_tmp) /* sessid len */
+#ifdef OSSL_ECH_SUPERVERBOSE
+ || (sessid_len = (size_t)pi_tmp) == 0
+#endif
+ || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* sessid */
+ || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite */
+ || !PACKET_get_1(&pkt, &pi_tmp) /* compression */
+ || (*exts = PACKET_data(&pkt) - shstart) == 0
+ || !PACKET_get_net_2(&pkt, &pi_tmp)) /* len(extensions) */
+ return 0;
+ extlens = (size_t)pi_tmp;
+ if (extlens == 0) /* not an error, in theory */
+ return 1;
+ estart = PACKET_data(&pkt);
+ while (PACKET_remaining(&pkt) > 0
+ && (size_t)(PACKET_data(&pkt) - estart) < extlens
+ && done < 1) {
+ if (!PACKET_get_net_2(&pkt, &etype)
+ || !PACKET_get_net_2(&pkt, &elen))
+ return 0;
+ if (etype == TLSEXT_TYPE_ech) {
+ if (elen == 0)
+ return 0;
+ *echoffset = PACKET_data(&pkt) - shstart - 4;
+ *echtype = etype;
+#ifdef OSSL_ECH_SUPERVERBOSE
+ echlen = elen + 4; /* type and length included */
+#endif
+ done++;
+ }
+ if (!PACKET_get_bytes(&pkt, &pp_tmp, elen))
+ return 0;
+ }
+#ifdef OSSL_ECH_SUPERVERBOSE
+ OSSL_TRACE_BEGIN(TLS)
+ {
+ BIO_printf(trc_out, "orig SH/ECH type: %4x\n", *echtype);
+ }
+ OSSL_TRACE_END(TLS);
+ ossl_ech_pbuf("orig SH", (unsigned char *)sh, sh_len);
+ ossl_ech_pbuf("orig SH session_id", (unsigned char *)sh + sessid_offset,
+ sessid_len);
+ ossl_ech_pbuf("orig SH exts", (unsigned char *)sh + *exts, extlens);
+ ossl_ech_pbuf("orig SH/ECH ", (unsigned char *)sh + *echoffset, echlen);
+#endif
+ return 1;
+}
+
+/* Do a HPKE seal of a padded encoded inner */
+static int seal_encoded_inner(char **out, int *outlen,
+ unsigned char *ei, size_t eilen,
+ const char *ch, int chlen,
+ size_t echoffset, size_t echlen)
+{
+ int res = 0;
+ OSSL_HPKE_CTX *hctx = NULL;
+ unsigned char *mypub = NULL;
+ static size_t mypublen = 0;
+ unsigned char *theirpub = NULL;
+ size_t theirpublen = 0;
+ unsigned char *ct = NULL;
+ size_t ctlen = 0;
+ unsigned char *aad = NULL;
+ size_t aadlen = 0;
+ unsigned char *chout = NULL;
+ size_t choutlen = 0;
+
+ hctx = OSSL_HPKE_CTX_new(OSSL_HPKE_MODE_BASE, hpke_suite,
+ OSSL_HPKE_ROLE_SENDER, NULL, NULL);
+ if (!TEST_ptr(hctx))
+ goto err;
+ mypublen = OSSL_HPKE_get_public_encap_size(hpke_suite);
+ if (!TEST_ptr(mypub = OPENSSL_malloc(mypublen)))
+ goto err;
+ theirpub = bin_echconfig + 11;
+ theirpublen = 0x20;
+ if (!TEST_true(OSSL_HPKE_encap(hctx, mypub, &mypublen,
+ theirpub, theirpublen,
+ hpke_info, hpke_infolen)))
+ goto err;
+ /* form up aad which is entire outer CH: zero's instead of ECH ciphertext */
+ choutlen = chlen;
+ if (!TEST_ptr(chout = OPENSSL_malloc(choutlen)))
+ goto err;
+ memcpy(chout, ch, chlen);
+ memcpy(chout + echoffset + 12, mypub, mypublen);
+ ct = chout + echoffset + 12 + mypublen + 2;
+ ctlen = OSSL_HPKE_get_ciphertext_size(hpke_suite, eilen);
+ chout[echoffset + 12 + mypublen] = (ctlen >> 8) & 0xff;
+ chout[echoffset + 12 + mypublen + 1] = ctlen & 0xff;
+ /* the 9 skips the record layer header */
+ aad = chout + SSL3_RT_HEADER_LENGTH + SSL3_HM_HEADER_LENGTH;
+ aadlen = chlen - (SSL3_RT_HEADER_LENGTH + SSL3_HM_HEADER_LENGTH);
+ if (short_test == 0 && ct + ctlen != aad + aadlen) {
+ TEST_info("length oddity");
+ goto err;
+ }
+ memset(ct, 0, ctlen);
+ if (!TEST_true(OSSL_HPKE_seal(hctx, ct, &ctlen, aad, aadlen, ei, eilen)))
+ goto err;
+ *out = (char *)chout;
+ *outlen = (int)choutlen;
+ res = 1;
+err:
+ OPENSSL_free(mypub);
+ OSSL_HPKE_CTX_free(hctx);
+ return res;
+}
+
+/* We'll either corrupt or copy the message based on the test index */
+static int corrupt_or_copy(const char *msg, const int msglen,
+ char **msgout, int *msgoutlen)
+{
+ TEST_ECHINNER *ti = NULL;
+ TEST_SH *ts = NULL;
+ TEST_ECHOUTER *to = NULL;
+ int is_ch = 0, is_sh = 0;
+ unsigned char *encoded_inner = NULL;
+ size_t prelen, fblen, postlen;
+ size_t encoded_innerlen = 0;
+ size_t sessid = 0, exts = 0, extlens = 0, echoffset = 0, echlen = 0;
+ size_t snioffset = 0, snilen = 0;
+ uint16_t echtype;
+ int inner, rv = 0;
+
+ /* is it a ClientHello or not? */
+ if (testcase == TESTCASE_CH && msglen > 10 && msg[0] == SSL3_RT_HANDSHAKE
+ && msg[5] == SSL3_MT_CLIENT_HELLO)
+ is_ch = 1;
+ /* is it a ServerHello or not? */
+ if (testcase == TESTCASE_SH && msglen > 10 && msg[0] == SSL3_RT_HANDSHAKE
+ && msg[5] == SSL3_MT_SERVER_HELLO)
+ is_sh = 1;
+ if (testcase == TESTCASE_ECH && msglen > 10 && msg[0] == SSL3_RT_HANDSHAKE
+ && msg[5] == SSL3_MT_CLIENT_HELLO)
+ is_ch = 1;
+
+ if (testcase == TESTCASE_CH && is_ch == 1) {
+ if (testiter >= (int)OSSL_NELEM(test_inners))
+ return 0;
+ ti = &test_inners[testiter];
+ prelen = ti->pre == NULL ? 0 : ti->prelen;
+ fblen = ti->forbork == NULL ? 0 : ti->fblen;
+ postlen = ti->post == NULL ? 0 : ti->postlen;
+ /* check for editing errors */
+ if (testiter != 0 && testiter != 1
+ && prelen + fblen + postlen != sizeof(entire_encoded_inner)) {
+ TEST_info("manual sizing error");
+ return 0;
+ }
+ if (testiter == 1) /* the only case with a short ciphertext for now */
+ short_test = 1;
+ if (!TEST_true(ossl_ech_helper_get_ch_offsets((const unsigned char *)msg
+ + SSL3_RT_HEADER_LENGTH
+ + SSL3_HM_HEADER_LENGTH,
+ msglen
+ - SSL3_RT_HEADER_LENGTH
+ - SSL3_HM_HEADER_LENGTH,
+ &sessid, &exts, &extlens,
+ &echoffset, &echtype, &echlen,
+ &snioffset, &snilen, &inner)))
+ return 0;
+ /* that better be an outer ECH :-) */
+ if (echoffset > 0 && !TEST_int_eq(inner, 0)) {
+ TEST_info("better send outer");
+ return 0;
+ }
+ /* bump offsets by 9 */
+ echoffset += 9;
+ snioffset += 9;
+ /*
+ * if it doesn't have an ECH, or if the forbork value in our test
+ * array is NULL, just copy the entire input to output
+ */
+ if (echoffset == 9 || ti->forbork == NULL) {
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ *msgoutlen = msglen;
+ return 1;
+ }
+ /* in this case, construct the encoded inner, then seal that */
+ encoded_innerlen = prelen + fblen + postlen;
+ if (!TEST_ptr(encoded_inner = OPENSSL_malloc(encoded_innerlen)))
+ return 0;
+ if (ti->pre != NULL) /* keep fuzz checker happy */
+ memcpy(encoded_inner, ti->pre, prelen);
+ if (ti->forbork != NULL)
+ memcpy(encoded_inner + prelen, ti->forbork, fblen);
+ if (ti->post != NULL)
+ memcpy(encoded_inner + prelen + fblen, ti->post, postlen);
+ if (!TEST_true(seal_encoded_inner(msgout, msgoutlen,
+ encoded_inner, encoded_innerlen,
+ msg, msglen, echoffset, echlen)))
+ return 0;
+ OPENSSL_free(encoded_inner);
+ return 1;
+ }
+
+ if (testcase == TESTCASE_ECH && is_ch == 1) {
+ if (testiter >= (int)OSSL_NELEM(test_echs))
+ return 0;
+ to = &test_echs[testiter];
+ if (!TEST_true(ossl_ech_helper_get_ch_offsets((const unsigned char *)msg
+ + SSL3_RT_HEADER_LENGTH
+ + SSL3_HM_HEADER_LENGTH,
+ msglen
+ - SSL3_RT_HEADER_LENGTH
+ - SSL3_HM_HEADER_LENGTH,
+ &sessid, &exts, &extlens,
+ &echoffset, &echtype, &echlen,
+ &snioffset, &snilen, &inner)))
+ return 0;
+ /* if it doesn't have an ECH just copy the entire input to output */
+ if (echoffset == 0) {
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ *msgoutlen = msglen;
+ return 1;
+ }
+ /* check for editing errors, the +4 is for ext type + len */
+ if (to->len > (echlen + 4)) {
+ TEST_info("manual sizing error");
+ return 0;
+ }
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ *msgoutlen = msglen;
+ /*
+ * overwrite (some of) the outer ECH, in contrast to
+ * the above case, here we're overwriting the ECH
+ * ext type and length as well, the +9 is for record
+ * layer framing as before
+ */
+ if (to->val != NULL) /* keep fuzz checker happy */
+ memcpy(*msgout + echoffset + 9, to->val, to->len);
+ return 1;
+ }
+
+ if (is_sh == 1) {
+ if (testiter >= (int)OSSL_NELEM(test_shs))
+ return 0;
+ ts = &test_shs[testiter];
+ if (ts->borkage == 0) {
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ *msgoutlen = msglen;
+ return 1;
+ }
+ /* flip bits in ECH confirmation */
+ if ((ts->borkage & OSSL_ECH_BORK_FLIP) != 0) {
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ if ((ts->borkage & OSSL_ECH_BORK_HRR) != 0) {
+ rv = ech_get_sh_offsets((unsigned char *)msg + 9,
+ msglen - 9,
+ &exts, &echoffset,
+ &echtype);
+ if (!TEST_int_eq(rv, 1))
+ return 0;
+ if (echoffset > 0) {
+ (*msgout)[9 + echoffset + 4] = (*msgout)[9 + echoffset + 4] ^ 0xaa;
+ }
+ } else {
+ (*msgout)[9 + 2 + SSL3_RANDOM_SIZE - 4] = (*msgout)[9 + 2 + SSL3_RANDOM_SIZE - 4] ^ 0xaa;
+ }
+ *msgoutlen = msglen;
+ return 1;
+ }
+ if ((ts->borkage & OSSL_ECH_BORK_REPLACE) != 0 && (ts->borkage & OSSL_ECH_BORK_HRR) != 0) {
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ rv = ech_get_sh_offsets((unsigned char *)msg + 9,
+ msglen - 9,
+ &exts, &echoffset, &echtype);
+ if (!TEST_int_eq(rv, 1))
+ return 0;
+ if (echoffset > 0)
+ memcpy(&((*msgout)[9 + echoffset]), ts->bork, ts->blen);
+ *msgoutlen = msglen;
+ return 1;
+ }
+ }
+ /* if doing nothing, do that... */
+ if (!TEST_ptr(*msgout = OPENSSL_memdup(msg, msglen)))
+ return 0;
+ *msgoutlen = msglen;
+ return 1;
+}
+
+static void copy_flags(BIO *bio)
+{
+ int flags;
+ BIO *next = BIO_next(bio);
+
+ flags = BIO_test_flags(next, BIO_FLAGS_SHOULD_RETRY | BIO_FLAGS_RWS);
+ BIO_clear_flags(bio, BIO_FLAGS_SHOULD_RETRY | BIO_FLAGS_RWS);
+ BIO_set_flags(bio, flags);
+}
+
+/*
+ * filter to corrupt or copy messages - this is basically copied
+ * from the setup in test/sslcorrupttest.c
+ */
+static int tls_corrupt_write(BIO *bio, const char *in, int inl)
+{
+ int ret;
+ BIO *next = BIO_next(bio);
+ char *copy = NULL;
+ int copylen = 0;
+
+ ret = corrupt_or_copy(in, inl, ©, ©len);
+ if (ret == 0)
+ return 0;
+ ret = BIO_write(next, copy, inl);
+ OPENSSL_free(copy);
+ copy_flags(bio);
+ return ret;
+}
+
+/*
+ * This and others below are NOOP filters as we only mess
+ * with things via the write filter method
+ */
+static int tls_noop_read(BIO *bio, char *out, int outl)
+{
+ int ret;
+ BIO *next = BIO_next(bio);
+
+ ret = BIO_read(next, out, outl);
+ copy_flags(bio);
+
+ return ret;
+}
+
+static long tls_noop_ctrl(BIO *bio, int cmd, long num, void *ptr)
+{
+ long ret;
+ BIO *next = BIO_next(bio);
+
+ if (next == NULL)
+ return 0;
+
+ switch (cmd) {
+ case BIO_CTRL_DUP:
+ ret = 0L;
+ break;
+ default:
+ ret = BIO_ctrl(next, cmd, num, ptr);
+ break;
+ }
+ return ret;
+}
+
+static int tls_noop_gets(BIO *bio, char *buf, int size)
+{
+ /* We don't support this - not needed anyway */
+ return -1;
+}
+
+static int tls_noop_puts(BIO *bio, const char *str)
+{
+ /* We don't support this - not needed anyway */
+ return -1;
+}
+
+static int tls_noop_new(BIO *bio)
+{
+ BIO_set_init(bio, 1);
+
+ return 1;
+}
+
+static int tls_noop_free(BIO *bio)
+{
+ BIO_set_init(bio, 0);
+
+ return 1;
+}
+
+#define BIO_TYPE_CUSTOM_CORRUPT (0x80 | BIO_TYPE_FILTER)
+#define BIO_TYPE_CUSTOM_SPLIT (0x81 | BIO_TYPE_FILTER)
+
+static BIO_METHOD *method_tls_corrupt = NULL;
+
+/* Note: Not thread safe! */
+static const BIO_METHOD *bio_f_tls_corrupt_filter(void)
+{
+ if (method_tls_corrupt == NULL) {
+ method_tls_corrupt = BIO_meth_new(BIO_TYPE_CUSTOM_CORRUPT,
+ "TLS corrupt filter");
+ if (method_tls_corrupt == NULL
+ || !BIO_meth_set_write(method_tls_corrupt, tls_corrupt_write)
+ || !BIO_meth_set_read(method_tls_corrupt, tls_noop_read)
+ || !BIO_meth_set_puts(method_tls_corrupt, tls_noop_puts)
+ || !BIO_meth_set_gets(method_tls_corrupt, tls_noop_gets)
+ || !BIO_meth_set_ctrl(method_tls_corrupt, tls_noop_ctrl)
+ || !BIO_meth_set_create(method_tls_corrupt, tls_noop_new)
+ || !BIO_meth_set_destroy(method_tls_corrupt, tls_noop_free))
+ return NULL;
+ }
+ return method_tls_corrupt;
+}
+
+static void bio_f_tls_corrupt_filter_free(void)
+{
+ BIO_meth_free(method_tls_corrupt);
+}
+
+static int test_ch_corrupt(int testidx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ BIO *c_to_s_fbio;
+ int testresult = 0, err = 0, connrv = 0, err_reason = 0;
+ int exp_err = SSL_ERROR_NONE;
+ TEST_ECHINNER *ti = NULL;
+ const char *err_str = NULL;
+
+ testcase = TESTCASE_CH;
+ testiter = testidx;
+ ti = &test_inners[testidx];
+ if (verbose)
+ TEST_info("Starting #%d", testidx + 1);
+ if (!TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ return 0;
+ /* set server to be willing to only accept TLSv1.2 for test case 23 */
+ if (testidx == 22
+ && !TEST_true(SSL_CTX_set_max_proto_version(sctx, TLS1_2_VERSION)))
+ goto end;
+ /* set server to be willing to accept TLSv1.2 for test case 24 */
+ if (testidx == 23
+ && !TEST_true(SSL_CTX_set_min_proto_version(sctx, TLS1_2_VERSION)))
+ goto end;
+ /* set client/server to be willing to accept TLSv1.2 for test case 25 */
+ if (testidx == 24
+ && !TEST_true(SSL_CTX_set_min_proto_version(sctx, TLS1_2_VERSION))
+ && !TEST_true(SSL_CTX_set_min_proto_version(cctx, TLS1_2_VERSION)))
+ goto end;
+ if (!TEST_true(SSL_CTX_set1_echstore(sctx, es)))
+ goto end;
+ if (!TEST_ptr(c_to_s_fbio = BIO_new(bio_f_tls_corrupt_filter())))
+ goto end;
+ /* BIO is freed by create_ssl_connection on error */
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL,
+ c_to_s_fbio)))
+ goto end;
+ if (!TEST_true(SSL_set1_ech_config_list(client, (unsigned char *)echconfig,
+ echconfiglen)))
+ goto end;
+ if (!TEST_true(SSL_set_tlsext_host_name(client, "foo.example.com")))
+ goto end;
+ exp_err = SSL_ERROR_SSL;
+ if (ti->err_expected == 0)
+ exp_err = SSL_ERROR_NONE;
+ connrv = create_ssl_connection(server, client, exp_err);
+ if (!TEST_int_eq(connrv, ti->rv_expected))
+ goto end;
+ if (verbose) {
+ err_str = ERR_reason_error_string(ti->err_expected);
+ err_reason = ERR_GET_REASON(ti->err_expected);
+ TEST_info("Expected error: %d/%s", err_reason, err_str);
+ }
+ if (connrv == 0) {
+ do {
+ err = ERR_get_error();
+ if (err == 0) {
+ TEST_error("ECH corruption: Unexpected error");
+ goto end;
+ }
+ err_reason = ERR_GET_REASON(err);
+ err_str = ERR_reason_error_string(err);
+ if (verbose)
+ TEST_info("Error reason: %d/%s", err_reason, err_str);
+ } while (err_reason != ti->err_expected);
+ }
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+static int test_sh_corrupt(int testidx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ BIO *s_to_c_fbio;
+ TEST_SH *ts = NULL;
+ int testresult = 0, err = 0, connrv = 0, err_reason = 0;
+ int exp_err = SSL_ERROR_NONE;
+ unsigned char *retryconfig = NULL;
+ size_t retryconfiglen = 0;
+ const char *err_str = NULL;
+
+ testcase = TESTCASE_SH;
+ testiter = testidx;
+ ts = &test_shs[testidx];
+ if (verbose)
+ TEST_info("Starting #%d", testidx + 1);
+ if (!TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ return 0;
+ if (!TEST_true(SSL_CTX_set1_echstore(sctx, es)))
+ goto end;
+ if (!TEST_ptr(s_to_c_fbio = BIO_new(bio_f_tls_corrupt_filter())))
+ goto end;
+ /* BIO is freed by create_ssl_connection on error */
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client,
+ s_to_c_fbio, NULL)))
+ goto end;
+ if ((ts->borkage & OSSL_ECH_BORK_GREASE) != 0) {
+ if (!TEST_true(SSL_set_options(client, SSL_OP_ECH_GREASE)))
+ goto end;
+ } else {
+ if (!TEST_true(SSL_set1_ech_config_list(client,
+ (unsigned char *)echconfig,
+ echconfiglen)))
+ goto end;
+ }
+ if (!TEST_true(SSL_set_tlsext_host_name(client, "foo.example.com")))
+ goto end;
+ if ((ts->borkage & OSSL_ECH_BORK_HRR) != 0
+ && !TEST_true(SSL_set1_groups_list(server, "P-384")))
+ goto end;
+ exp_err = SSL_ERROR_SSL;
+ if (ts->err_expected == 0)
+ exp_err = SSL_ERROR_NONE;
+ connrv = create_ssl_connection(server, client, exp_err);
+ if (!TEST_int_eq(connrv, ts->rv_expected))
+ goto end;
+ if (connrv == 1 && (ts->borkage & OSSL_ECH_BORK_GREASE) != 0) {
+ if (!TEST_true(SSL_ech_get1_retry_config(client, &retryconfig,
+ &retryconfiglen))
+ || !TEST_ptr(retryconfig)
+ || !TEST_int_ne((int)retryconfiglen, 0))
+ goto end;
+ }
+ if (verbose) {
+ err_str = ERR_reason_error_string(ts->err_expected);
+ err_reason = ERR_GET_REASON(ts->err_expected);
+ TEST_info("Expected error: %d/%s", err_reason, err_str);
+ }
+ if (connrv == 0) {
+ do {
+ err = ERR_get_error();
+ if (err == 0) {
+ TEST_error("ECH corruption: Unexpected error");
+ goto end;
+ }
+ err_reason = ERR_GET_REASON(err);
+ err_str = ERR_reason_error_string(err);
+ if (verbose)
+ TEST_info("Error reason: %d/%s", err_reason, err_str);
+ } while (err_reason != ts->err_expected);
+ }
+ testresult = 1;
+end:
+ OPENSSL_free(retryconfig);
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+typedef enum OPTION_choice {
+ OPT_ERR = -1,
+ OPT_EOF = 0,
+ OPT_VERBOSE,
+ OPT_TEST_ENUM
+} OPTION_CHOICE;
+
+static int test_ech_corrupt(int testidx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *server = NULL, *client = NULL;
+ BIO *c_to_s_fbio;
+ int testresult = 0, err = 0, connrv = 0, err_reason = 0;
+ int exp_err = SSL_ERROR_NONE;
+ TEST_ECHOUTER *to = NULL;
+ const char *err_str = NULL;
+
+ testcase = TESTCASE_ECH;
+ testiter = testidx;
+ to = &test_echs[testidx];
+ if (verbose)
+ TEST_info("Starting #%d", testidx + 1);
+ if (!TEST_true(create_ssl_ctx_pair(NULL, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ return 0;
+ if (!TEST_true(SSL_CTX_set1_echstore(sctx, es)))
+ goto end;
+ if (!TEST_ptr(c_to_s_fbio = BIO_new(bio_f_tls_corrupt_filter())))
+ goto end;
+ /* BIO is freed by create_ssl_connection on error */
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &server, &client, NULL,
+ c_to_s_fbio)))
+ goto end;
+ if (!TEST_true(SSL_set1_ech_config_list(client, (unsigned char *)echconfig,
+ echconfiglen)))
+ goto end;
+ if (!TEST_true(SSL_set_tlsext_host_name(client, "foo.example.com")))
+ goto end;
+ exp_err = SSL_ERROR_SSL;
+ if (to->err_expected == 0)
+ exp_err = SSL_ERROR_NONE;
+ connrv = create_ssl_connection(server, client, exp_err);
+ if (!TEST_int_eq(connrv, to->rv_expected))
+ goto end;
+ if (verbose) {
+ err_str = ERR_reason_error_string(to->err_expected);
+ err_reason = ERR_GET_REASON(to->err_expected);
+ TEST_info("Expected error: %d/%s", err_reason, err_str);
+ }
+ if (connrv == 0) {
+ do {
+ err = ERR_get_error();
+ if (err == 0) {
+ TEST_error("ECH corruption: Unexpected error");
+ goto end;
+ }
+ err_reason = ERR_GET_REASON(err);
+ err_str = ERR_reason_error_string(err);
+ if (verbose)
+ TEST_info("Error reason: %d/%s", err_reason, err_str);
+ } while (err_reason != to->err_expected);
+ }
+ testresult = 1;
+end:
+ SSL_free(server);
+ SSL_free(client);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+
+const OPTIONS *test_get_options(void)
+{
+ static const OPTIONS test_options[] = {
+ OPT_TEST_OPTIONS_DEFAULT_USAGE,
+ { "v", OPT_VERBOSE, '-', "Enable verbose mode" },
+ { OPT_HELP_STR, 1, '-', "Run ECH Corruption tests\n" },
+ { NULL }
+ };
+ return test_options;
+}
+
+int setup_tests(void)
+{
+ OPTION_CHOICE o;
+ BIO *in = NULL;
+
+ while ((o = opt_next()) != OPT_EOF) {
+ switch (o) {
+ case OPT_VERBOSE:
+ verbose = 1;
+ break;
+ case OPT_TEST_CASES:
+ break;
+ default:
+ return 0;
+ }
+ }
+ if (!test_skip_common_options()) {
+ TEST_error("Error parsing test options\n");
+ return 0;
+ }
+ certsdir = test_get_argument(0);
+ if (certsdir == NULL)
+ certsdir = DEF_CERTS_DIR;
+ cert = test_mk_file_path(certsdir, "servercert.pem");
+ if (cert == NULL)
+ goto err;
+ privkey = test_mk_file_path(certsdir, "serverkey.pem");
+ if (privkey == NULL)
+ goto err;
+
+ /* make an OSSL_ECHSTORE for pem_kp1 */
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp1, (int)strlen(pem_kp1)) <= 0
+ || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY)))
+ goto err;
+ BIO_free_all(in);
+ in = NULL;
+ hpke_infolen = bin_echconfiglen + 200;
+ if (!TEST_ptr(hpke_info = OPENSSL_malloc(hpke_infolen)))
+ goto err;
+ /* +/- 2 is to drop the ECHConfigList length at the start */
+ if (!TEST_true(ossl_ech_make_enc_info((unsigned char *)bin_echconfig + 2,
+ bin_echconfiglen - 2,
+ hpke_info, &hpke_infolen)))
+ goto err;
+ ADD_ALL_TESTS(test_ch_corrupt, OSSL_NELEM(test_inners));
+ ADD_ALL_TESTS(test_sh_corrupt, OSSL_NELEM(test_shs));
+ ADD_ALL_TESTS(test_ech_corrupt, OSSL_NELEM(test_echs));
+ return 1;
+err:
+ BIO_free_all(in);
+ return 0;
+}
+
+void cleanup_tests(void)
+{
+ bio_f_tls_corrupt_filter_free();
+ OPENSSL_free(cert);
+ OPENSSL_free(privkey);
+ OPENSSL_free(hpke_info);
+ OSSL_ECHSTORE_free(es);
+}
diff --git a/test/ech_test.c b/test/ech_test.c
index 6db85bc668..4098c218e2 100644
--- a/test/ech_test.c
+++ b/test/ech_test.c
@@ -26,13 +26,11 @@ static char *privkey = NULL;
static char *rootcert = NULL;
static int ch_test_cb_ok = 0;
-/* TODO(ECH): add some testing of SSL_OP_ECH_IGNORE_CID */
-
/* ECH callback */
static unsigned int ech_test_cb(SSL *s, const char *str)
{
if (verbose)
- TEST_info("ech_test_cb called");
+ TEST_info("ech_test_cb called: str=\n%s", str);
return 1;
}
@@ -171,6 +169,28 @@ static const char pem_kp1[] = "-----BEGIN PRIVATE KEY-----\n"
"AQALZXhhbXBsZS5jb20AAA==\n"
"-----END ECHCONFIG-----\n";
+/*
+ * x25519 ech key pair with public key front.server.example, used for
+ * in_out test
+ */
+static const char pem_kp2[] = "-----BEGIN PRIVATE KEY-----\n"
+ "MC4CAQAwBQYDK2VuBCIEIEjRn9R/gwDu11v6bLKaf0AGoe5Etl2g6nU1GdQLTHNe\n"
+ "-----END PRIVATE KEY-----\n"
+ "-----BEGIN ECHCONFIG-----\n"
+ "AEf+DQBDvQAgACCr9pErR7E/gNeoni+0YpDZaMd7XN+hFnCN+H0Xnm1EHQAEAAEAAQAUZnJvbnQuc2VydmVyLmV4YW1wbGUAAA==\n"
+ "-----END ECHCONFIG-----\n";
+static const char ec_kp2[] = "AEf+DQBDvQAgACCr9pErR7E/gNeoni+0YpDZaMd7XN+hFnCN+H0Xnm1EHQAEAAEAAQAUZnJvbnQuc2VydmVyLmV4YW1wbGUAAA==";
+static size_t ec_kp2len = sizeof(ec_kp2) - 1;
+
+/* another, used in grease/retry-config tests, pn: f1.server.example */
+static const char pem_kp3[] = "-----BEGIN PRIVATE KEY-----\n"
+ "MC4CAQAwBQYDK2VuBCIEIDDbFFGbdUUQgKJzx6zaqn0rE8Bi9DJQpkfbAL6HHwtu\n"
+ "-----END PRIVATE KEY-----\n"
+ "-----BEGIN ECHCONFIG-----\n"
+ "AET+DQBAYQAgACAUpnXhRlufbhe61F02+NR7xVA3200ujwOp2JLivunoQAAEAAEA\n"
+ "AQARZjEuc2VydmVyLmV4YW1wbGUAAA==\n"
+ "-----END ECHCONFIG-----\n";
+
/* standard x25519 ECHConfigList with public key example.com */
static const char pem_pk1[] = "-----BEGIN ECHCONFIG-----\n"
"AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
@@ -814,11 +834,11 @@ typedef struct FNT_T {
} fnt_t;
static fnt_t fnames[] = {
- { "ech-eg.pem", 1 },
- { "ech-mid.pem", 1 },
- { "ech-big.pem", 1 },
- { "ech-giant.pem", 0 },
- { "ech-rsa.pem", 0 },
+ { "echdir/ech-eg.pem", 1 },
+ { "echdir/ech-mid.pem", 1 },
+ { "echdir/ech-big.pem", 1 },
+ { "echdir/ech-giant.pem", 0 },
+ { "echdir/ech-rsa.pem", 0 },
};
/* string from which we construct varieties of HPKE suite */
@@ -1087,9 +1107,6 @@ static int ech_api_basic_calls(void)
SSL_CTX_ech_set_callback(NULL, NULL);
SSL_ech_set_callback(NULL, NULL);
if (!TEST_false(SSL_ech_get1_retry_config(NULL, NULL, NULL))
- || !TEST_false(SSL_CTX_ech_raw_decrypt(NULL, NULL, NULL, NULL,
- NULL, 0, NULL, NULL,
- NULL, NULL))
|| !TEST_int_eq(SSL_ech_get1_status(NULL, &rinner, &router),
SSL_ECH_STATUS_FAILED))
goto end;
@@ -1193,6 +1210,7 @@ end:
#define OSSL_ECH_TEST_ENOE 4 /* early + no-ech */
#define OSSL_ECH_TEST_CBS 5 /* test callbacks */
#define OSSL_ECH_TEST_V12 6 /* test TLSv1.2 */
+#define OSSL_ECH_TEST_NO_INNER 7 /* test no inner SNI */
/* note: early-data is prohibited after HRR so no tests for that */
/*
@@ -1239,6 +1257,12 @@ static int test_ech_roundtrip_helper(int idx, int combo)
kdf_str_list[kdfind], aead_str_list[aeadind]);
if (verbose)
TEST_info("Doing: iter: %d, suite: %s", idx, suitestr);
+ /*
+ * Set a max name length just to exercise more code.
+ * We may as well use the index, just to make it vary:-)
+ */
+ if (combo == OSSL_ECH_TEST_NO_INNER)
+ max_name_length = (idx % 256);
if (!TEST_true(OSSL_HPKE_str2suite(suitestr, &hpke_suite))
|| !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
|| !TEST_true(OSSL_ECHSTORE_new_config(es, ech_version, max_name_length,
@@ -1286,6 +1310,8 @@ static int test_ech_roundtrip_helper(int idx, int combo)
if (combo != OSSL_ECH_TEST_ENOE
&& !TEST_true(SSL_CTX_set1_echstore(cctx, es)))
goto end;
+ /* set callback for client, just to exercise code */
+ SSL_CTX_ech_set_callback(cctx, ech_test_cb);
if (!TEST_true(SSL_CTX_set1_echstore(sctx, es))
|| !TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
&clientssl, NULL, NULL)))
@@ -1293,7 +1319,8 @@ static int test_ech_roundtrip_helper(int idx, int combo)
if (combo == OSSL_ECH_TEST_HRR
&& !TEST_true(SSL_set1_groups_list(serverssl, "P-384")))
goto end;
- if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
+ if (combo != OSSL_ECH_TEST_NO_INNER
+ && !TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
goto end;
if (combo == OSSL_ECH_TEST_V12) {
if (!TEST_false(create_ssl_connection(serverssl, clientssl,
@@ -1330,7 +1357,8 @@ static int test_ech_roundtrip_helper(int idx, int combo)
goto end;
/* all good */
if (combo == OSSL_ECH_TEST_BASIC || combo == OSSL_ECH_TEST_HRR
- || combo == OSSL_ECH_TEST_CUSTOM || combo == OSSL_ECH_TEST_CBS) {
+ || combo == OSSL_ECH_TEST_CUSTOM || combo == OSSL_ECH_TEST_CBS
+ || combo == OSSL_ECH_TEST_NO_INNER) {
res = 1;
goto end;
}
@@ -1423,6 +1451,14 @@ static int test_ech_hrr(int idx)
return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_HRR);
}
+/* ECH with no inner SNI for the given suite */
+static int test_ech_no_inner(int idx)
+{
+ if (verbose)
+ TEST_info("Doing: test_ech_no_inner");
+ return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_NO_INNER);
+}
+
/* ECH with early data for the given suite */
static int test_ech_early(int idx)
{
@@ -1463,6 +1499,451 @@ static int ech_v12_test(int idx)
return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_V12);
}
+/*
+ * Test roundtrip with SNI/ALPN variations.
+ * Inner and outer names can be supplied to SSL_CTX or SSL
+ * connection via ECH APIs, and inner can be supplied via
+ * the existing non-ECH API. We can specify that no outer
+ * SNI at all be sent if we want. If an outer SNI value is
+ * supplied via the ECH API then that over-rides the
+ * public_name field from the ECHConfig, which in this
+ * cases will be example.com. We have the option of setting
+ * both inner, outer and no_outer setting via eiher:
+ *
+ * int SSL_ech_set1_server_names(SSL *s, const char *inner_name,
+ * const char *outer_name, int no_outer);
+ * int SSL_ech_set1_outer_server_name(SSL *s, const char *outer_name,
+ * int no_outer);
+ *
+ * So there's a bunch of cases to test, as usual we pick
+ * between 'em using the idx parameter.
+ *
+ * idx : case
+ * 0 : set no names via ECH APIs;
+ * set inner to inner.example.com non-ECH API
+ * expect public_name as outer
+ * 1 : as for 0, but additionally:
+ * set NULL and "no_outer" via set_outer API
+ * 2 : as for 1, but additionally:
+ * set non-NULL outer and "no_outer" via set_outer API
+ * 3 : override outer via ECH API
+ * 4 : like 1, but using set_server_names API
+ * 5 : like 2, but using set_server_names API
+ * 6 : like 3, but using set_server_names API
+ * 7 : like 4, but overriding previous call to non-ECH SNI
+ * 8 : like 5, but overriding previous call to non-ECH SNI
+ * 9 : like 6, but overriding previous call to non-ECH SNI
+ * 10 : like 7, but reversing call order
+ * 11 : like 8, but reversing call order
+ * 12 : like 9, but reversing call order
+ * 13 : like 1, but with a NULL outer input to API
+ * that's a bit pointless as it's more or less a NO-OP
+ * but worth checking
+ */
+static int ech_in_out_test(int idx)
+{
+ int res = 0, cres = 0, sres = 0, clientstatus, serverstatus;
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL;
+ char *non_ech_sni = "trad.server.example"; /* SNI set via non-ECH API */
+ char *supplied_inner = "inner.server.example"; /* inner set via ECH API */
+ char *supplied_outer = "outer.server.example"; /* outer set via ECH API */
+ char *public_name = "front.server.example"; /* we know that's inside pem_kp2 */
+ char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL;
+ unsigned char alpn_inner[] = { /* "inner, secret, http/1.1" */
+ 0x05, 0x69, 0x6e, 0x6e, 0x65, 0x72,
+ 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
+ 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31
+ };
+ size_t alpn_inner_len = sizeof(alpn_inner);
+ unsigned char alpn_outer[] = { /* "outer, public, h2" */
+ 0x05, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x06, 0x70,
+ 0x75, 0x62, 0x6c, 0x69, 0x63, 0x02, 0x68, 0x32
+ };
+ size_t alpn_outer_len = sizeof(alpn_outer);
+ char *expected_inner = NULL, *expected_outer = NULL;
+ BIO *in = NULL;
+ OSSL_ECHSTORE *es = NULL;
+
+ /* make an OSSL_ECHSTORE for pem_kp2 */
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp2, (int)strlen(pem_kp2)) <= 0
+ || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY))
+ || !TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey))
+ || !TEST_true(SSL_CTX_set1_echstore(sctx, es))
+ || !TEST_false(SSL_CTX_set_alpn_protos(cctx, alpn_inner,
+ (unsigned int)alpn_inner_len))
+ || !TEST_true(SSL_CTX_ech_set1_outer_alpn_protos(cctx, alpn_outer,
+ alpn_outer_len))
+ || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL))
+ || !TEST_true(SSL_set1_ech_config_list(clientssl,
+ (unsigned char *)ec_kp2,
+ ec_kp2len)))
+ goto end;
+ if (idx == 0) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = public_name;
+ }
+ if (idx == 1) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni))
+ || !TEST_true(SSL_ech_set1_outer_server_name(clientssl, NULL, 1)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = NULL;
+ }
+ if (idx == 2) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni))
+ || !TEST_true(SSL_ech_set1_outer_server_name(clientssl, "blah", 1)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = NULL;
+ }
+ if (idx == 3) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni))
+ || !TEST_true(SSL_ech_set1_outer_server_name(clientssl,
+ supplied_outer, 0))
+ || !TEST_true(SSL_ech_set1_outer_alpn_protos(clientssl, alpn_outer,
+ alpn_outer_len)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = supplied_outer;
+ }
+ if (idx == 4) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl,
+ supplied_inner, NULL, 0)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = public_name;
+ }
+ if (idx == 5) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ "blah", 1)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = NULL;
+ }
+ if (idx == 6) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ supplied_outer, 0)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = supplied_outer;
+ }
+ if (idx == 7) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "blah"))
+ || !TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ NULL, 0)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = public_name;
+ }
+ if (idx == 8) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "blah"))
+ || !TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ "blah", 1)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = NULL;
+ }
+ if (idx == 9) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "blah"))
+ || !TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ supplied_outer, 0)))
+ goto end;
+ expected_inner = supplied_inner;
+ expected_outer = supplied_outer;
+ }
+ if (idx == 10) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl,
+ supplied_inner, NULL, 0))
+ || !TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = public_name;
+ }
+ if (idx == 11) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ "blah", 1))
+ || !TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = NULL;
+ }
+ if (idx == 12) {
+ if (!TEST_true(SSL_ech_set1_server_names(clientssl, supplied_inner,
+ supplied_outer, 0))
+ || !TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = supplied_outer;
+ }
+ if (idx == 13) {
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, non_ech_sni))
+ || !TEST_true(SSL_ech_set1_outer_server_name(clientssl, NULL, 0)))
+ goto end;
+ expected_inner = non_ech_sni;
+ expected_outer = public_name;
+ }
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+ serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ SSL_set_verify_result(clientssl, X509_V_OK); /* override cert check */
+ clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
+ if (!TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ cres = sres = 0; /* check result vs. expected */
+ if ((expected_inner == NULL && cinner == NULL)
+ || (expected_inner != NULL && cinner != NULL
+ && strlen(expected_inner) == strlen(cinner)
+ && strcmp(expected_inner, cinner) == 0))
+ cres = 1;
+ if (!TEST_int_eq(cres, 1))
+ goto end;
+ if ((expected_inner == NULL && sinner == NULL)
+ || (expected_inner != NULL && sinner != NULL
+ && strlen(expected_inner) == strlen(sinner)
+ && strcmp(expected_inner, sinner) == 0))
+ sres = 1;
+ if (!TEST_int_eq(sres, 1))
+ goto end;
+ cres = sres = 0;
+ if ((expected_outer == NULL && couter == NULL)
+ || (expected_outer != NULL && couter != NULL
+ && strlen(expected_outer) == strlen(couter)
+ && strcmp(expected_outer, couter) == 0))
+ cres = 1;
+ if (!TEST_int_eq(cres, 1))
+ goto end;
+ if ((expected_outer == NULL && souter == NULL)
+ || (expected_outer != NULL && souter != NULL
+ && strlen(expected_outer) == strlen(souter)
+ && strcmp(expected_outer, souter) == 0))
+ sres = 1;
+ if (!TEST_int_eq(sres, 1))
+ goto end;
+ res = 1; /* all good */
+end:
+ OSSL_ECHSTORE_free(es);
+ BIO_free_all(in);
+ OPENSSL_free(sinner);
+ OPENSSL_free(souter);
+ OPENSSL_free(cinner);
+ OPENSSL_free(couter);
+ SSL_free(clientssl);
+ SSL_free(serverssl);
+ SSL_CTX_free(cctx);
+ SSL_CTX_free(sctx);
+ return res;
+}
+
+/* Test roundtrip with GREASE'd ECH, then again with retry-config */
+static int ech_grease_test(int idx)
+{
+ int res = 0, clientstatus, serverstatus;
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL;
+ char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL;
+ unsigned char *retryconfig = NULL;
+ size_t retryconfiglen = 0;
+ X509_STORE *ch = NULL;
+ OSSL_ECHSTORE *es = NULL;
+ BIO *in;
+
+ /* make OSSL_ECHSTORE vars for pem_kp2 */
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp2, (int)strlen(pem_kp2)) <= 0
+ || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq))
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY)))
+ goto end;
+ if (idx == 2) {
+ /*
+ * In our last test iteration set various other ECH configs, to make
+ * for a bigger retry-config. (It's ok that we set the same key pair
+ * a few times here.)
+ */
+ BIO_free_all(in);
+ in = NULL;
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp3, (int)strlen(pem_kp3)) <= 0
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY)))
+ goto end;
+ BIO_free_all(in);
+ in = NULL;
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp3, (int)strlen(pem_kp3)) <= 0
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY)))
+ goto end;
+ BIO_free_all(in);
+ in = NULL;
+ if ((in = BIO_new(BIO_s_mem())) == NULL
+ || BIO_write(in, pem_kp3, (int)strlen(pem_kp3)) <= 0
+ || !TEST_true(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY)))
+ goto end;
+ }
+ BIO_free_all(in);
+ in = NULL;
+
+ if (!TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(),
+ TLS_client_method(),
+ TLS1_3_VERSION, TLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ goto end;
+ if (!TEST_true(SSL_CTX_set1_echstore(sctx, es)))
+ goto end;
+
+ /* set the client GREASE flag via SSL_CTX 1st time, and via SSL* 2nd */
+ if (idx == 0 && !TEST_true(SSL_CTX_set_options(cctx, SSL_OP_ECH_GREASE)))
+ goto end;
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL)))
+ goto end;
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "back.server.example")))
+ goto end;
+
+ /* set the GREASE flag via SSL_CTX 1st time, and via SSL* 2nd & 3rd */
+ if (idx >= 1 && !TEST_true(SSL_set_options(clientssl, SSL_OP_ECH_GREASE)))
+ goto end;
+ /* 3rd time, fail to set a bad grease suite, then set a good one */
+ if (idx == 2 && !TEST_false(SSL_ech_set1_grease_suite(clientssl, "notanhpkesuite")))
+ goto end;
+ if (idx == 2 && !TEST_true(SSL_ech_set1_grease_suite(clientssl, "x25519,2,3")))
+ goto end;
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+ serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+ if (verbose)
+ TEST_info("ech_grease_test: server status %d, %s, %s",
+ serverstatus, sinner, souter);
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_GREASE))
+ goto end;
+ /* override cert verification */
+ SSL_set_verify_result(clientssl, X509_V_OK);
+ clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
+ if (verbose)
+ TEST_info("ech_grease_test: client status %d, %s, %s",
+ clientstatus, cinner, couter);
+ if (!TEST_int_eq(clientstatus, SSL_ECH_STATUS_GREASE_ECH))
+ goto end;
+ if (!TEST_true(SSL_ech_get1_retry_config(clientssl, &retryconfig,
+ &retryconfiglen)))
+ goto end;
+ if (!TEST_ptr(retryconfig))
+ goto end;
+ if (!TEST_int_ne((int)retryconfiglen, 0))
+ goto end;
+ if (verbose)
+ TEST_info("ech_grease_test: retryconfglen: %d\n", (int)retryconfiglen);
+ /* we kow the sizes to expect as the configs are hard-coded above */
+ if (idx == 2 && !TEST_size_t_eq(retryconfiglen, 141))
+ goto end;
+ if (idx < 2 && !TEST_size_t_eq(retryconfiglen, 73))
+ goto end;
+ /* cleanup */
+ OPENSSL_free(sinner);
+ OPENSSL_free(souter);
+ OPENSSL_free(cinner);
+ OPENSSL_free(couter);
+ sinner = souter = cinner = couter = NULL;
+ SSL_shutdown(clientssl);
+ SSL_shutdown(serverssl);
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ serverssl = clientssl = NULL;
+
+ /* second connection */
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL)))
+ goto end;
+ /* setting an ECHConfig should over-ride GREASE flag */
+ if (!TEST_true(SSL_set1_ech_config_list(clientssl, retryconfig,
+ retryconfiglen)))
+ goto end;
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
+ goto end;
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+ serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+ if (verbose)
+ TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ /* override cert verification */
+ SSL_set_verify_result(clientssl, X509_V_OK);
+ clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
+ if (verbose)
+ TEST_info("client status %d, %s, %s", clientstatus, cinner, couter);
+ if (!TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ /* 3rd connection - this time grease+HRR which had a late fail */
+ OPENSSL_free(sinner);
+ OPENSSL_free(souter);
+ OPENSSL_free(cinner);
+ OPENSSL_free(couter);
+ sinner = souter = cinner = couter = NULL;
+ SSL_shutdown(clientssl);
+ SSL_shutdown(serverssl);
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ serverssl = clientssl = NULL;
+ if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+ &clientssl, NULL, NULL)))
+ goto end;
+ /* force ECH+HRR */
+ if (!TEST_true(SSL_set_options(clientssl, SSL_OP_ECH_GREASE)))
+ goto end;
+ /* setting an ECHConfig should over-ride GREASE flag */
+ if (!TEST_true(SSL_set1_ech_config_list(clientssl, retryconfig,
+ retryconfiglen)))
+ goto end;
+ if (!TEST_true(SSL_set1_groups_list(serverssl, "P-384")))
+ goto end;
+ if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
+ goto end;
+ if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+ SSL_ERROR_NONE)))
+ goto end;
+ serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+ if (verbose)
+ TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
+ if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ /* override cert verification */
+ SSL_set_verify_result(clientssl, X509_V_OK);
+ clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
+ if (verbose)
+ TEST_info("client status %d, %s, %s", clientstatus, cinner, couter);
+ if (!TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+ goto end;
+ /* all good */
+ res = 1;
+end:
+ OPENSSL_free(sinner);
+ OPENSSL_free(souter);
+ OPENSSL_free(cinner);
+ OPENSSL_free(couter);
+ OPENSSL_free(retryconfig);
+ SSL_free(clientssl);
+ SSL_free(serverssl);
+ X509_STORE_free(ch);
+ SSL_CTX_free(cctx);
+ SSL_CTX_free(sctx);
+ OSSL_ECHSTORE_free(es);
+ BIO_free_all(in);
+ return res;
+}
+
#endif
int setup_tests(void)
@@ -1508,7 +1989,9 @@ int setup_tests(void)
ADD_ALL_TESTS(ech_enoe_test, suite_combos);
ADD_ALL_TESTS(ech_cb_test, suite_combos);
ADD_ALL_TESTS(ech_v12_test, suite_combos);
- /* TODO(ECH): add more test code as other PRs done */
+ ADD_ALL_TESTS(ech_in_out_test, 14);
+ ADD_ALL_TESTS(ech_grease_test, 3);
+ ADD_ALL_TESTS(test_ech_no_inner, suite_combos);
return 1;
err:
return 0;
diff --git a/test/recipes/20-test_app_ech.t b/test/recipes/20-test_app_ech.t
index 60a3b96309..b6b5ead315 100644
--- a/test/recipes/20-test_app_ech.t
+++ b/test/recipes/20-test_app_ech.t
@@ -19,7 +19,7 @@ plan skip_all => "ECH tests not supported in this build"
if disabled("ech") || disabled("tls1_3")
|| disabled("ec") || disabled("ecx");
-plan tests => 13;
+plan tests => 16;
ok(run(app(["openssl", "ech", "-help"])),
"Run openssl ech with help");
@@ -90,4 +90,17 @@ with({ exit_checker => sub { return shift == 1; } },
"-in", "eg3.pem",
"-in", "eg4.pem"])),
"Too many input files");
+ ok(run(app(["openssl", "ech" ])),
+ "No input files");
+ ok(run(app(["openssl", "ech",
+ "-public_name", "example.com",
+ "-out", ""
+ ])),
+ "(Fail to) Generate an ECH key pair to empty output file name");
+ ok(run(app(["openssl", "ech",
+ "-in", "eg1.pem",
+ "-in", "eg2.pem",
+ "-out", "",
+ "-verbose"])),
+ "Fail to catenate due to empty output file name");
});
diff --git a/test/recipes/30-test_ech.t b/test/recipes/30-test_ech.t
index 73ecd2a9c6..6f789e3c6a 100644
--- a/test/recipes/30-test_ech.t
+++ b/test/recipes/30-test_ech.t
@@ -1,5 +1,5 @@
#! /usr/bin/env perl
-# Copyright 2022 The OpenSSL Project Authors. All Rights Reserved.
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
#
# Licensed under the Apache License 2.0 (the "License"). You may not use
diff --git a/test/recipes/30-test_ech_corrupt.t b/test/recipes/30-test_ech_corrupt.t
new file mode 100644
index 0000000000..9bb11545c2
--- /dev/null
+++ b/test/recipes/30-test_ech_corrupt.t
@@ -0,0 +1,25 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+# Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use strict;
+use OpenSSL::Test::Utils;
+use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_dir bldtop_file/;
+
+setup("test_ech_corrupt");
+
+# Seeing tls1_2 below may be unexpected but we include a test case
+# where the inner CH is TLSv1.2 and the outer is TLSv1.3, but we
+# don't get the expected error in builds where TLSv1.2 is not supported
+# so we'll skip those
+plan skip_all => "ECH tests not supported in this build"
+ if disabled("ech") || disabled("tls1_2") || disabled("tls1_3") || disabled("ec") || disabled("ecx");
+
+plan tests => 1;
+
+ok(run(test(["ech_corrupt_test", srctop_dir("test", "certs")])))
diff --git a/test/recipes/75-test_quicapi.t b/test/recipes/75-test_quicapi.t
index 7056ad17b0..b2fdbb0f5d 100644
--- a/test/recipes/75-test_quicapi.t
+++ b/test/recipes/75-test_quicapi.t
@@ -25,6 +25,17 @@ plan skip_all => "QUIC protocol is not supported by this OpenSSL build"
plan skip_all => "These tests are not supported in a fuzz build"
if config('options') =~ /-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION|enable-fuzz-afl/;
+# When we support ECH, the ECH compression scheme affects the ordering
+# of extensions in the ClientHello as the set of compressed extensions
+# need to be contiguous in the outer ClientHello. We would need different
+# trace files to compare against for this test in ECH builds vs. 'no-ech'
+# buiids, so we'll just skip this test in 'no-ech' builds..
+# Note that that ordering will be affected if the ECH compression
+# choices are changed - see the comments in ssl/statem/extensions.c
+# where those choices are embedded in the ext_defs table.
+plan skip_all => "QUIC API trace tests aren't done in no-ech builds"
+ if disabled('ech');
+
plan tests =>
($no_fips ? 0 : 1) # quicapitest with fips
+ 1; # quicapitest with default provider
diff --git a/test/recipes/82-test_ech_client_server.t b/test/recipes/82-test_ech_client_server.t
index 9197c52f65..65d6b05e45 100644
--- a/test/recipes/82-test_ech_client_server.t
+++ b/test/recipes/82-test_ech_client_server.t
@@ -10,7 +10,7 @@ use strict;
use warnings;
use IPC::Open3;
-use OpenSSL::Test qw/:DEFAULT srctop_file bldtop_file/;
+use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_file/;
use OpenSSL::Test::Utils;
use Symbol 'gensym';
@@ -23,6 +23,8 @@ my $s_client_match = 0;
my $test_name = "test_ech_client_server";
setup($test_name);
+plan skip_all => "$test_name requires ECH"
+ if disabled("ech");
plan skip_all => "$test_name requires EC cryptography"
if disabled("ec") || disabled("ecx");
plan skip_all => "$test_name requires sock enabled"
@@ -32,16 +34,17 @@ plan skip_all => "$test_name requires TLSv1.3 enabled"
plan skip_all => "$test_name is not available Windows or VMS"
if $^O =~ /^(VMS|MSWin32|msys)$/;
-plan tests => 18;
+plan tests => 22;
my $shlib_wrap = bldtop_file("util", "shlib_wrap.sh");
my $apps_openssl = bldtop_file("apps", "openssl");
-my $echconfig_pem = srctop_file("test", "certs", "ech-eg.pem");
-my $badconfig_pem = srctop_file("test", "certs", "ech-mid.pem");
+my $echconfig_pem = srctop_file("test", "certs", "echdir", "ech-eg.pem");
+my $badconfig_pem = srctop_file("test", "certs", "echdir", "ech-mid.pem");
my $server_pem = srctop_file("test", "certs", "echserver.pem");
my $server_key = srctop_file("test", "certs", "echserver.key");
my $root_pem = srctop_file("test", "certs", "rootcert.pem");
+my $ech_dir = srctop_dir("test", "certs", "echdir" );
sub extract_ecl()
{
@@ -88,6 +91,15 @@ sub start_ech_client_server
"-servername", "example.com",
"-ech_trialdecrypt",
"-tls1_3");
+ } elsif ($test_type eq "keydir" ) {
+ # load keys from key dir (some will fail)
+ @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+ "-cert", $server_pem, "-key", $server_key,
+ "-cert2", $server_pem, "-key2", $server_key,
+ "-ech_dir", $ech_dir,
+ "-ech_noretry_dir", $ech_dir,
+ "-servername", "example.com",
+ "-tls1_3");
} else {
# default for all other tests (for now)
@s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
@@ -117,6 +129,8 @@ sub start_ech_client_server
last;
} elsif (/^Using default/) {
;
+ } elsif (/^Added (\d+) ECH/) {
+ ;
} elsif (/^Added ECH key pair/) {
;
} elsif (/^Loaded/) {
@@ -134,13 +148,21 @@ sub start_ech_client_server
# -prexit
my @s_client_cmd;
if ($test_type eq "GREASE-suite" ) {
- # GREASE
+ # GREASE with suite
@s_client_cmd = ("s_client",
"-connect", "localhost:$s_server_port",
"-servername", "server.example",
"-CAfile", $root_pem,
"-ech_grease_suite", "0x21,2,3",
"-prexit");
+ } elsif ($test_type eq "bad-GREASE-suite" ) {
+ # bad GREASE suite
+ @s_client_cmd = ("s_client",
+ "-connect", "localhost:$s_server_port",
+ "-servername", "server.example",
+ "-CAfile", $root_pem,
+ "-ech_grease_suite", "thisisnotagoodone",
+ "-prexit");
} elsif ($test_type eq "lots-of-options" ) {
# real ECH with lots of options
@s_client_cmd = ("s_client",
@@ -154,7 +176,7 @@ sub start_ech_client_server
"-ech_outer_alpn", "http451",
"-prexit");
} elsif ($test_type eq "GREASE-type" ) {
- # GREASE with suite
+ # GREASE with type
@s_client_cmd = ("s_client",
"-connect", "localhost:$s_server_port",
"-servername", "server.example",
@@ -280,6 +302,16 @@ sub grease_suite_test {
ok($s_client_match == 1, "s_client with GREASE-suite ECH");
}
+sub bad_grease_suite_test {
+ print("\n\nGREASE suite ECHConfig test.\n");
+ my $tt="bad-GREASE-suite";
+ my $win="^ECH: NOT CONFIGURED";
+ start_ech_client_server($tt, $win);
+ ok($s_server_port ne "0", "s_server port check");
+ print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+ ok($s_client_match == 1, "s_client with bad GREASE-suite ECH");
+}
+
sub grease_type_test {
print("\n\nGREASE type ECH test.\n");
my $tt="GREASE-type";
@@ -330,13 +362,25 @@ sub cid_wrong_test {
ok($s_client_match == 1, "s_client/s_server with no CID/no trial decrypt");
}
+sub keydir_test {
+ print("\n\nServer using key dir test.\n");
+ my $tt = "keydir";
+ my $win = "^ECH: success";
+ start_ech_client_server($tt, $win);
+ ok($s_server_port ne "0", "s_server port check");
+ print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+ ok($s_client_match == 1, "s_server using ech keydir on command line");
+}
+
basic_test();
wrong_test();
grease_test();
grease_suite_test();
+bad_grease_suite_test();
grease_type_test();
lots_of_options_test();
no_outer_test();
cid_free_test();
cid_wrong_test();
+keydir_test();
diff --git a/test/recipes/95-test_external_ech_bssl.t b/test/recipes/95-test_external_ech_bssl.t
new file mode 100644
index 0000000000..858ab480d6
--- /dev/null
+++ b/test/recipes/95-test_external_ech_bssl.t
@@ -0,0 +1,29 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use OpenSSL::Test;
+use OpenSSL::Test::Utils;
+use OpenSSL::Test qw/:DEFAULT data_file bldtop_dir srctop_dir cmdstr/;
+
+setup("test_external_ech_bssl");
+
+plan skip_all => "No external tests in this configuration"
+ if disabled("external-tests");
+plan skip_all => "External ECH tests not available on Windows or VMS"
+ if $^O =~ /^(VMS|MSWin32)$/;
+plan skip_all => "External ECH tests not supported in out of tree builds"
+ if bldtop_dir() ne srctop_dir();
+
+plan tests => 2;
+
+ok(run(cmd(["sh", data_file("ech_bssl_external.sh")])),
+ "running ECH client external boringssl tests");
+
+ok(run(cmd(["sh", data_file("ech_bssl_server_external.sh")])),
+ "running ECH server external boringssl tests");
diff --git a/test/recipes/95-test_external_ech_bssl_data/ech_bssl_external.sh b/test/recipes/95-test_external_ech_bssl_data/ech_bssl_external.sh
new file mode 100755
index 0000000000..ba4451a0dd
--- /dev/null
+++ b/test/recipes/95-test_external_ech_bssl_data/ech_bssl_external.sh
@@ -0,0 +1,101 @@
+#!/bin/sh
+
+#
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+# OpenSSL ECH external testing using boringssl client
+
+PWD="$(pwd)"
+
+SRCTOP="$(cd $SRCTOP; pwd)"
+BLDTOP="$(cd $BLDTOP; pwd)"
+
+if [ "$SRCTOP" != "$BLDTOP" ] ; then
+ echo "Out of tree builds not supported with ECH external test!"
+ exit 1
+fi
+
+O_EXE="$BLDTOP/apps"
+O_BINC="$BLDTOP/include"
+O_SINC="$SRCTOP/include"
+O_LIB="$BLDTOP"
+
+unset OPENSSL_CONF
+
+export PATH="$O_EXE:$PATH"
+export LD_LIBRARY_PATH="$O_LIB:$LD_LIBRARY_PATH"
+export OPENSSL_ROOT_DIR="$O_LIB"
+
+# Check/Set openssl version
+OPENSSL_VERSION=`openssl version | cut -f 2 -d ' '`
+ECHCONFIGFILE=$SRCTOP/test/certs/echdir/ech-eg.pem
+httphost=server.example
+httpreq="GET /stats HTTP/1.1\\r\\nConnection: close\\r\\nHost: $httphost\\r\\n\\r\\n"
+BTOOL=$SRCTOP/boringssl/.local/bin
+
+echo "------------------------------------------------------------------"
+echo "Testing OpenSSL s_server using ECH-enabled boringssl client:"
+echo " CWD: $PWD"
+echo " SRCTOP: $SRCTOP"
+echo " BLDTOP: $BLDTOP"
+echo " OPENSSL_ROOT_DIR: $OPENSSL_ROOT_DIR"
+echo " OpenSSL version: $OPENSSL_VERSION"
+echo " PEM ECH Config file: $ECHCONFIGFILE"
+
+echo "------------------------------------------------------------------"
+
+if [ ! -d $SRCTOP/boringssl ]; then
+ mkdir -p $SRCTOP/boringssl
+fi
+if [ ! -d $SRCTOP/boringssl/.local ]; then
+(
+ cd $SRCTOP \
+ && git clone https://boringssl.googlesource.com/boringssl \
+ && cd boringssl \
+ && mkdir build \
+ && cd build \
+ && cmake -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR -DCMAKE_INSTALL_PREFIX=$SRCTOP/boringssl/.local .. \
+ && make \
+ && make install
+ )
+fi
+
+echo " CWD: $PWD"
+
+# Start an openssl s_server
+$SRCTOP/apps/openssl s_server \
+ -key $SRCTOP/test/certs/echserver.key -cert $SRCTOP/test/certs/echserver.pem \
+ -key2 $SRCTOP/test/certs/echserver.key -cert2 $SRCTOP/test/certs/echserver.pem \
+ -CAfile $SRCTOP/test/certs/rootcert.pem \
+ -ech_key $ECHCONFIGFILE -port 8443 -tls1_3 -WWW \
+ -naccept 1 -ign_eof -servername server.example &
+pids=`ps -ef | grep s_server | grep -v grep | awk '{print $2}'`
+if [ -z "$pids" ]
+then
+ echo "No sign of s_server - exiting (before client)"
+ exit 88
+fi
+# We want the binary ECHConfigList for bssl so extract that
+# from the PEM file
+bechfile=`mktemp`
+resfile=`mktemp`
+cat $ECHCONFIGFILE \
+ | awk '/^-----BEGIN ECHCONFIG/,/^-----END ECHCONFIG/' \
+ | tail -n+2 | head -n+2 | tr -d '\n' | base64 -d >$bechfile
+echo "Running bssl s_client against localhost"
+(echo -e $httpreq ; sleep 2) | \
+ $BTOOL/bssl s_client -connect localhost:8443 \
+ -ech-config-list $bechfile \
+ -server-name $httphost \
+ -root-certs $SRCTOP/test/certs/rootcert.pem > $resfile 2>&1
+rm -f $bechfile
+cat $resfile
+success=`grep -c "Encrypted ClientHello: yes" $resfile`
+rm -f $resfile
+# if success==1 we're good so exit with a zero for test success
+exit $((success != 1))
diff --git a/test/recipes/95-test_external_ech_bssl_data/ech_bssl_server_external.sh b/test/recipes/95-test_external_ech_bssl_data/ech_bssl_server_external.sh
new file mode 100755
index 0000000000..770ed27e98
--- /dev/null
+++ b/test/recipes/95-test_external_ech_bssl_data/ech_bssl_server_external.sh
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+#
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+# OpenSSL ECH external testing using boringssl server
+
+PWD="$(pwd)"
+
+SRCTOP="$(cd $SRCTOP; pwd)"
+BLDTOP="$(cd $BLDTOP; pwd)"
+
+if [ "$SRCTOP" != "$BLDTOP" ] ; then
+ echo "Out of tree builds not supported with ECH external test!"
+ exit 1
+fi
+
+O_EXE="$BLDTOP/apps"
+O_BINC="$BLDTOP/include"
+O_SINC="$SRCTOP/include"
+O_LIB="$BLDTOP"
+
+unset OPENSSL_CONF
+
+export PATH="$O_EXE:$PATH"
+export LD_LIBRARY_PATH="$O_LIB:$LD_LIBRARY_PATH"
+export OPENSSL_ROOT_DIR="$O_LIB"
+
+# Check/Set openssl version
+OPENSSL_VERSION=`openssl version | cut -f 2 -d ' '`
+ECHCONFIGFILE=$SRCTOP/test/certs/echdir/ech-eg.pem
+httphost=server.example
+httpreq="GET /stats HTTP/1.1\\r\\nConnection: close\\r\\nHost: $httphost\\r\\n\\r\\n"
+BTOOL=$SRCTOP/boringssl/.local/bin
+
+echo "------------------------------------------------------------------"
+echo "Testing ECH-enabled boringssl server using s_client:"
+echo " CWD: $PWD"
+echo " SRCTOP: $SRCTOP"
+echo " BLDTOP: $BLDTOP"
+echo " OPENSSL_ROOT_DIR: $OPENSSL_ROOT_DIR"
+echo " OpenSSL version: $OPENSSL_VERSION"
+echo " PEM ECH Config file: $ECHCONFIGFILE"
+
+echo "------------------------------------------------------------------"
+
+if [ ! -d $SRCTOP/boringssl ]; then
+ mkdir -p $SRCTOP/boringssl
+fi
+if [ ! -d $SRCTOP/boringssl/.local ]; then
+(
+ cd $SRCTOP \
+ && git clone https://boringssl.googlesource.com/boringssl \
+ && cd boringssl \
+ && mkdir build \
+ && cd build \
+ && cmake -DOPENSSL_ROOT_DIR=$OPENSSL_ROOT_DIR -DCMAKE_INSTALL_PREFIX=$SRCTOP/boringssl/.local .. \
+ && make \
+ && make install
+ )
+fi
+
+echo " CWD: $PWD"
+
+bssllist=`mktemp`
+bsslech=`mktemp`
+bsslkey=`mktemp`
+bsslpem=`mktemp`
+echo "Generating ECH keys for a bssl s_server."
+$BTOOL/bssl generate-ech -out-ech-config-list $bssllist \
+ -out-ech-config $bsslech -out-private-key $bsslkey \
+ -public-name example.com -config-id 222 -max-name-length 0
+res=$?
+# the b64 form is friendlier for s_client
+cat $bssllist | base64 -w0 >$bsslpem
+
+# Start a boringssl s_server
+$BTOOL/bssl s_server \
+ -accept 8443 \
+ -key $SRCTOP/test/certs/echserver.key -cert $SRCTOP/test/certs/echserver.pem \
+ -ech-config $bsslech -ech-key $bsslkey \
+ -www -loop &
+pids=`ps -ef | grep 'bssl s_server' | grep -v grep | awk '{print $2}'`
+if [ -z "$pids" ]
+then
+ echo "No sign of s_server - exiting (before client)"
+ rm -f $bssllist $bsslech $bsslkey $bsslpem
+ exit 88
+fi
+echo "Running openssl s_client against localhost"
+(echo -e $httpreq ; sleep 2) | \
+ $SRCTOP/apps/openssl s_client -connect localhost:8443 \
+ -CAfile $SRCTOP/test/certs/rootcert.pem \
+ -ech_config_list `cat $bsslpem` \
+ -servername $httphost \
+ -tls1_3 -ignore_unexpected_eof
+success=$?
+# bssl server has to be killed
+kill $pids
+rm -f $bssllist $bsslech $bsslkey $bsslpem
+# s_client returns 1 if ok, we want to exit with 0 for a PASS
+exit $success
diff --git a/test/recipes/95-test_external_ech_nss.t b/test/recipes/95-test_external_ech_nss.t
new file mode 100644
index 0000000000..ced0e72836
--- /dev/null
+++ b/test/recipes/95-test_external_ech_nss.t
@@ -0,0 +1,31 @@
+#! /usr/bin/env perl
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use OpenSSL::Test;
+use OpenSSL::Test::Utils;
+use OpenSSL::Test qw/:DEFAULT data_file bldtop_dir srctop_dir cmdstr/;
+
+setup("test_external_ech_nss");
+
+plan skip_all => "No external tests in this configuration"
+ if disabled("external-tests");
+plan skip_all => "External ECH tests not available on Windows or VMS"
+ if $^O =~ /^(VMS|MSWin32)$/;
+ #plan skip_all => "External ECH tests only available in a shared build"
+ #if disabled("shared");
+plan skip_all => "External ECH tests not supported in out of tree builds"
+ if bldtop_dir() ne srctop_dir();
+
+plan tests => 2;
+
+ok(run(cmd(["sh", data_file("ech_nss_external.sh")])),
+ "running ECH client external NSS tests");
+
+ok(run(cmd(["sh", data_file("ech_nss_server_external.sh")])),
+ "running ECH server external NSS tests");
diff --git a/test/recipes/95-test_external_ech_nss_data/ech_nss_external.sh b/test/recipes/95-test_external_ech_nss_data/ech_nss_external.sh
new file mode 100755
index 0000000000..cf3648e93c
--- /dev/null
+++ b/test/recipes/95-test_external_ech_nss_data/ech_nss_external.sh
@@ -0,0 +1,132 @@
+#!/usr/bin/env bash
+
+#
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+# OpenSSL ECH external testing using nss client
+
+PWD="$(pwd)"
+
+SRCTOP="$(cd $SRCTOP; pwd)"
+BLDTOP="$(cd $BLDTOP; pwd)"
+
+if [ "$SRCTOP" != "$BLDTOP" ] ; then
+ echo "Out of tree builds not supported with ECH external test!"
+ exit 1
+fi
+
+O_EXE="$BLDTOP/apps"
+O_BINC="$BLDTOP/include"
+O_SINC="$SRCTOP/include"
+O_LIB="$BLDTOP"
+
+unset OPENSSL_CONF
+
+export PATH="$O_EXE:$PATH"
+export LD_LIBRARY_PATH="$O_LIB:$LD_LIBRARY_PATH"
+export OPENSSL_ROOT_DIR="$O_LIB"
+
+# Check/Set openssl version
+OPENSSL_VERSION=`openssl version | cut -f 2 -d ' '`
+ECHCONFIGFILE=$SRCTOP/test/certs/echdir/ech-eg.pem
+httphost=server.example
+httpreq="GET /stats HTTP/1.1\\r\\nConnection: close\\r\\nHost: $httphost\\r\\n\\r\\n"
+LDIR=$SRCTOP/nss/dist/Debug/bin
+NLIB=$SRCTOP/nss/dist/Debug/lib
+
+echo "------------------------------------------------------------------"
+echo "Testing OpenSSL s_server using ECH-enabled nss client:"
+echo " CWD: $PWD"
+echo " SRCTOP: $SRCTOP"
+echo " BLDTOP: $BLDTOP"
+echo " OPENSSL_ROOT_DIR: $OPENSSL_ROOT_DIR"
+echo " OpenSSL version: $OPENSSL_VERSION"
+echo " PEM ECH Config file: $ECHCONFIGFILE"
+
+echo "------------------------------------------------------------------"
+
+if [ ! -d $SRCTOP/nss ]; then
+ mkdir -p $SRCTOP/nss
+fi
+
+# clone our NSS and NSPR
+if [ ! -d $SRCTOP/nss/nss ]; then
+ cd $SRCTOP/nss && git clone https://github.com/nss-dev/nss.git
+fi
+if [ ! -d $SRCTOP/nss/nspr ]; then
+ cd $SRCTOP/nss && hg clone https://hg.mozilla.org/projects/nspr
+fi
+
+# build - used to use build.sh but there's some problem with an
+# tmp awk script running within the make environment so we'll do
+# things in a more basic fashion
+TPATH=""
+if [ -d "$SRCTOP/nss/dist" ]
+then
+ TPATH="$(find $SRCTOP/nss/dist -name tstclnt)"
+fi
+if [ -z "$TPATH" ]
+then
+ ( cd $SRCTOP/nss/nss && USE_64=1 make nss_build_all )
+ ( cd $SRCTOP/nss/nss && USE_64=1 make install )
+fi
+TPATH="$(find $SRCTOP/nss/dist -name tstclnt)"
+if [ -z "$TPATH" ]
+then
+ echo "Build failed - exiting"
+ exit 99
+fi
+LDIR=$(dirname "$TPATH")
+NLIB="$LDIR/../lib"
+
+if [ ! -f $LDIR/tstclnt ]; then
+ echo "Failed to build NSS - exiting"
+ exit 99
+fi
+if [ ! -f $LDIR/certutil ]; then
+ echo "Failed to build NSS - exiting"
+ exit 99
+fi
+
+# If we have an NSS build, create an NSS DB for our fake root so we can
+# use NSS' tstclnt to talk to our s_server.
+if [ -f $LDIR/certutil ]
+then
+ mkdir -p $SRCTOP/nss/ca
+ LD_LIBRARY_PATH=$NLIB $LDIR/certutil -A \
+ -i $SRCTOP/test/certs/rootcert.pem \
+ -n "oe" -t "CT,C,C" -d $SRCTOP/nss/ca/
+fi
+
+echo " CWD: $PWD"
+
+# Start an openssl s_server
+$SRCTOP/apps/openssl s_server \
+ -key $SRCTOP/test/certs/echserver.key -cert $SRCTOP/test/certs/echserver.pem \
+ -key2 $SRCTOP/test/certs/echserver.key -cert2 $SRCTOP/test/certs/echserver.pem \
+ -CAfile $SRCTOP/test/certs/rootcert.pem \
+ -ech_key $ECHCONFIGFILE -port 8443 -tls1_3 -WWW -naccept 1 \
+ -ign_eof -servername server.example &
+pids=`ps -ef | grep s_server | grep -v grep | awk '{print $2}'`
+if [ -z "$pids" ]
+then
+ echo "No sign of s_server - exiting (before client)"
+ exit 88
+fi
+
+# to ensure we detect a fail, use the wrong ECHConfig ...
+# ECHCONFIGFILE=$SRCTOP/esnistuff/d13.pem
+ECH=`cat $ECHCONFIGFILE \
+ | awk '/^-----BEGIN ECHCONFIG/,/^-----END ECHCONFIG/' \
+ | tail -n+2 | head -n+2 | tr -d '\n'`
+
+NSSPARAMS="-Q -4 -b -d $SRCTOP/nss/ca"
+LD_LIBRARY_PATH="$NLIB" $LDIR/tstclnt $NSSPARAMS -h localhost -p 8443 -a $httphost -N $ECH
+res=$?
+exit $res
+
diff --git a/test/recipes/95-test_external_ech_nss_data/ech_nss_server_external.sh b/test/recipes/95-test_external_ech_nss_data/ech_nss_server_external.sh
new file mode 100755
index 0000000000..f6d296f4b7
--- /dev/null
+++ b/test/recipes/95-test_external_ech_nss_data/ech_nss_server_external.sh
@@ -0,0 +1,162 @@
+#!/bin/sh
+
+#
+# Copyright 2025 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+#
+# OpenSSL ECH external testing using nss
+
+PWD="$(pwd)"
+
+SRCTOP="$(cd $SRCTOP; pwd)"
+BLDTOP="$(cd $BLDTOP; pwd)"
+
+if [ "$SRCTOP" != "$BLDTOP" ] ; then
+ echo "Out of tree builds not supported with ECH external test!"
+ exit 1
+fi
+
+O_EXE="$BLDTOP/apps"
+O_BINC="$BLDTOP/include"
+O_SINC="$SRCTOP/include"
+O_LIB="$BLDTOP"
+
+unset OPENSSL_CONF
+
+export PATH="$O_EXE:$PATH"
+export LD_LIBRARY_PATH="$O_LIB:$LD_LIBRARY_PATH"
+export OPENSSL_ROOT_DIR="$O_LIB"
+
+# Check/Set openssl version
+OPENSSL_VERSION=`openssl version | cut -f 2 -d ' '`
+ECHCONFIGFILE=$SRCTOP/test/certs/echdir/ech-eg.pem
+httphost=server.example
+httpreq="GET / HTTP/1.1\\r\\nConnection: close\\r\\nHost: $httphost\\r\\n\\r\\n"
+
+echo "------------------------------------------------------------------"
+echo "Testing OpenSSL s_client using ECH-enabled nss server:"
+echo " CWD: $PWD"
+echo " SRCTOP: $SRCTOP"
+echo " BLDTOP: $BLDTOP"
+echo " OPENSSL_ROOT_DIR: $OPENSSL_ROOT_DIR"
+echo " OpenSSL version: $OPENSSL_VERSION"
+echo " PEM ECH Config file: $ECHCONFIGFILE"
+
+echo "------------------------------------------------------------------"
+
+if [ ! -d $SRCTOP/nss ]; then
+ mkdir -p $SRCTOP/nss
+fi
+# clone our NSS and NSPR
+if [ ! -d $SRCTOP/nss/nss ]; then
+ (
+ cd $SRCTOP/nss \
+ && git clone https://github.com/nss-dev/nss.git
+ )
+fi
+if [ ! -d $SRCTOP/nss/nspr ]; then
+ (
+ cd $SRCTOP/nss \
+ && hg clone https://hg.mozilla.org/projects/nspr
+ )
+fi
+
+# build - used to use build.sh but there's some problem with an
+# tmp awk script running within the make environment so we'll do
+# things in a more basic fashion
+TPATH=""
+if [ -d "$SRCTOP/nss/dist" ]
+then
+ TPATH="$(find $SRCTOP/nss/dist -name tstclnt)"
+fi
+if [ -z "$TPATH" ]
+then
+ ( cd $SRCTOP/nss/nss && USE_64=1 make nss_build_all )
+ ( cd $SRCTOP/nss/nss && USE_64=1 make install )
+fi
+TPATH="$(find $SRCTOP/nss/dist -name tstclnt)"
+if [ -z "$TPATH" ]
+then
+ echo "Build failed - exiting"
+ exit 99
+fi
+LDIR=$(dirname "$TPATH")
+LLIB="$LDIR/../lib"
+
+if [ ! -f $LDIR/selfserv ]; then
+ echo "Failed to build NSS - exiting"
+ exit 99
+fi
+if [ ! -f $LDIR/certutil ]; then
+ echo "Failed to build NSS - exiting"
+ exit 99
+fi
+
+# If we have an NSS build, create an NSS DB for our fake root so we can
+# use NSS' tstclnt to talk to our s_server.
+if [ ! -d $SRCTOP/nss/server ]
+then
+ mkdir -p $SRCTOP/nss/server
+ LD_LIBRARY_PATH=$LLIB $LDIR/certutil -A \
+ -i $SRCTOP/test/certs/rootcert.pem \
+ -n "oe" -t "CT,C,C" -d $SRCTOP/nss/server/
+ sillypass="sillypass"
+ $SRCTOP/apps/openssl pkcs12 -export -out tmp.p12 \
+ -inkey $SRCTOP/test/certs/echserver.key \
+ -in $SRCTOP/test/certs/echserver.pem \
+ -password "pass:$sillypass"
+ echo -n $sillypass >sillypassfile
+ LD_LIBRARY_PATH=$LLIB $LDIR/pk12util \
+ -i tmp.p12 -d $SRCTOP/nss/server -w sillypassfile
+ cat sillypassfile
+ # rm -f sillypassfile tmp.p12
+fi
+
+echo " CWD: $PWD"
+
+# Start an NSS server
+# We'll let the server generate the ECH key pair for now (see
+# below for why).
+
+# need to use ``stdbuf -o0`` so that we don't get buffering and
+# can grab echconfig immediately...
+LD_LIBRARY_PATH=$LLIB stdbuf -o0 $LDIR/selfserv -p 8443 -d $SRCTOP/nss/server \
+ -n server.example -X "publicname:example.com" >ss-echfile &
+if [ -s ss-echfile ]
+then
+ echo "Did you remember to patch NSS? See $0 for details"
+ exit 78
+fi
+
+# For the future, we'd like a provide our private-key/ECHConfig to
+# NSS - looks like there could be some work required to get that
+# selfserve option working.
+# https://bugzilla.mozilla.org/show_bug.cgi?id=1876732
+
+pids=`ps -ef | grep selfserv | grep -v grep | awk '{print $2}'`
+if [ -z "$pids" ]
+then
+ echo "No sign of selfserv - exiting (before client)"
+ exit 88
+fi
+
+# to ensure we detect a fail, use the wrong ECHConfig ...
+# ECHCONFIGFILE=$SRCTOP/esnistuff/d13.pem
+# ECH=`cat $ECHCONFIGFILE | tail -2 | head -1`
+ECH=`cat ss-echfile | tail -2 | head -1`
+echo "Running openssl s_client against localhost"
+(echo -e $httpreq ; sleep 2) | \
+ $SRCTOP/apps/openssl s_client -connect localhost:8443 \
+ -CAfile $SRCTOP/test/certs/rootcert.pem \
+ -ech_config_list $ECH \
+ -servername $httphost \
+ -no_ssl3 -no_tls1 -no_tls1_1 -no_tls1_2
+success=$?
+# nss server needs killing
+kill $pids
+exit $success
diff --git a/util/libssl.num b/util/libssl.num
index 9f67f9949b..eb376dc72c 100644
--- a/util/libssl.num
+++ b/util/libssl.num
@@ -629,7 +629,6 @@ SSL_get1_echstore ? 4_0_0 EXIST::FUNCTION:ECH
SSL_ech_get1_status ? 4_0_0 EXIST::FUNCTION:ECH
SSL_ech_set_grease_type ? 4_0_0 EXIST::FUNCTION:ECH
SSL_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH
-SSL_CTX_ech_raw_decrypt ? 4_0_0 EXIST::FUNCTION:ECH
SSL_CTX_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH
OSSL_ECHSTORE_num_entries ? 4_0_0 EXIST::FUNCTION:ECH
SSL_ech_set1_server_names ? 4_0_0 EXIST::FUNCTION:ECH