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)
This commit is contained in:
sftcd 2025-11-23 23:19:16 +00:00 committed by Matt Caswell
parent 0bdae603e8
commit 5e5a76fc2c
44 changed files with 3505 additions and 313 deletions

8
.gitignore vendored
View file

@ -425,3 +425,11 @@ doc-nits
# LSP (Language Server Protocol) support
.cache/
compile_commands.json
# coverage files
*.gcda
*.gcno
lcov.info
run_tests
depend

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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.
Almost all ECH code is protected via `#ifndef OPENSSL_NO_ECH` and there is a
`no-ech` option to build without this code.
BoringSSL APIs
--------------
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.
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.)
ECH Tests
---------
Just as our implementation is under development, BoringSSL's
`include/openssl/ssl.h` says: "ECH support in BoringSSL is still experimental
and under development."
The following tests are included in the `make test` target:
### GREASE
- [`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)
BoringSSL uses an API to enable GREASEing rather than an option.
There are also two external tests to check interoperability
with the NSS and BoringSSL libraries:
```c
OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
- [`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
```
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.
To clean away the coverage files:
### 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`.
```c
OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl,
const char **out_name,
size_t *out_name_len);
```bash
find . -name '*.gcda' -exec rm {} \;
find . -name '*.gcno' -exec rm {} \;
rm lcov.info
make clean
make -j12
```
I'm not sure how this compares. Need to investigate.
Checking memory errors
----------------------
### Create an ECHConfigList
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.
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`
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:
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.
```bash
#!/bin/bash
#
# run 16k tests
```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);
# 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
```
Collectively these are similar to `OSSL_ECH_make_echconfig()`.
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.
### Setting ECH keys on a server
You need to build with the `crypto-mdebug` option to get the memory allocation
failure, so that'd be something like:
Again using the `SSL_ECH_KEYS` type and APIs, servers can build up a set of
ECH keys using:
```c
OPENSSL_EXPORT int SSL_CTX_set1_ech_keys(SSL_CTX *ctx, SSL_ECH_KEYS *keys);
```bash
./config --debug enable-external-tests enable-crypto-mdebug --coverage no-asm no-afalgeng no-shared -DPEDANTIC -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
```
### Getting status
BoringSSL has:
```c
OPENSSL_EXPORT int SSL_ech_accepted(const SSL *ssl);
```
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.

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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.
the concept of an B<OSSL_ECHSTORE> which contains ECH configuration
information relevant for an B<SSL_CTX> or B<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
This release only supports ECH shared-mode and has no support for
ECH split-mode.
=head2 Callback Function
=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(), 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_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_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

View file

@ -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);

View file

@ -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)

View file

@ -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);

View file

@ -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 */

View file

@ -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) {

View file

@ -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) {

View file

@ -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)

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEIPiqiukxstIS9ViT4zWlewgPbjN4XVQ1XrcYgxwE6sl1
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AED+CgA8vwAgACCB9YyilgR2NMLVPOsESVceIrfGpXThGIUMIwGfGClmSgAEAAEAAQAAAAtleGFtcGxlLmNvbQAA
-----END ECHCONFIG-----

View file

@ -0,0 +1,8 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfVyqIGiyInH7KDB3
szOuOFYP41zV2gpA8wESUjpcqrChRANCAAQaLs1qXa+t31TtQeD6hXwWoltwJgL1
zt9kMsrj1HMScUQ8yIpNt+KUjzuMLbJxG9SpuZqfFW/agXJlyQEmatGE
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AGH+CgBd0QAQAEEEGi7Nal2vrd9U7UHg+oV8FqJbcCYC9c7fZDLK49RzEnFEPMiKTbfilI87jC2ycRvUqbmanxVv2oFyZckBJmrRhAAEAAEAAQAAAAtleGFtcGxlLmNvbQAA
-----END ECHCONFIG-----

View file

@ -0,0 +1 @@
AD7+DQA6uAAgACAogff+HZbirYdQCfXI01GBPP8AEKYyK/D/0DoeXD84fgAQAAEAAQgLZXhhbUNwbGUuYwYAAAAAQwA=

View file

@ -0,0 +1,7 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VuBCIEILCwRG3jPpLC4JRINol54Jo8bvua3e1EV3EDbepDCmpi
-----END PRIVATE KEY-----
-----BEGIN ECHCONFIG-----
AD7+DQA6IwAgACDT+hqR+Atl9LejS6nJrr9oDspGvwWTGOW3WJHdJy+OSQAEAAEA
ASALZXhhbXBsZS5jb20AAA==
-----END ECHCONFIG-----

View file

@ -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-----

1691
test/ech_corrupt_test.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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");
});

View file

@ -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

View file

@ -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")])))

View file

@ -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

View file

@ -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();

View file

@ -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");

View file

@ -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))

View file

@ -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

View file

@ -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");

View file

@ -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

View file

@ -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

View file

@ -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