diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl index 7fdb0b86eb87a..3b8fee0171ca8 100644 --- a/Configurations/unix-Makefile.tmpl +++ b/Configurations/unix-Makefile.tmpl @@ -1349,7 +1349,8 @@ errors: include/openssl/dtls1.h include/openssl/srtp.h include/openssl/quic.h - include/openssl/sslerr_legacy.h ); + include/openssl/sslerr_legacy.h + include/openssl/ech.h); my @cryptoheaders_tmpl = qw( include/internal/dso.h include/internal/o_dir.h diff --git a/Configure b/Configure index 2d89667176d16..0127e3e55dd18 100755 --- a/Configure +++ b/Configure @@ -472,6 +472,7 @@ my @disablables = ( "ecdh", "ecdsa", "ecx", + "ech", "egd", "engine", "err", @@ -635,7 +636,7 @@ my @disable_cascades = ( "blake2", "bf", "camellia", "cast", "chacha", "cmac", "cms", "cmp", "comp", "ct", "des", "dgram", "dh", "dsa", - "ec", "engine", + "ec", "ech", "engine", "filenames", "idea", "ktls", "lms", "md4", "ml-dsa", "ml-kem", "multiblock", diff --git a/INSTALL.md b/INSTALL.md index f90b937cef682..b167e078fcbb1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -800,6 +800,11 @@ Disable legacy TLS EC groups that were deprecated in RFC8422. These are the Koblitz curves, B, B, B, B, and the binary Elliptic curves that would also be disabled by C. +### no-ech + +Don't build support for Encrypted Client Hello (ECH) extension (draft-ietf-tls-esni) +TODO(ECH) update link to RFC. + ### enable-ec_nistp_64_gcc_128 Enable support for optimised implementations of some commonly used NIST diff --git a/apps/build.info b/apps/build.info index 345f7079584a5..d6835829e4bac 100644 --- a/apps/build.info +++ b/apps/build.info @@ -18,6 +18,7 @@ $OPENSSLSRC=\ pkcs8.c pkey.c pkeyparam.c pkeyutl.c prime.c rand.c req.c \ s_client.c s_server.c s_time.c sess_id.c skeyutl.c smime.c speed.c \ spkac.c verify.c version.c x509.c rehash.c storeutl.c \ + ech.c \ list.c info.c fipsinstall.c pkcs12.c IF[{- !$disabled{'ec'} -}] $OPENSSLSRC=$OPENSSLSRC ec.c ecparam.c diff --git a/apps/ech.c b/apps/ech.c new file mode 100644 index 0000000000000..bb52b9aa32760 --- /dev/null +++ b/apps/ech.c @@ -0,0 +1,277 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include +#include +#include +#include "apps.h" +#include "progs.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef OPENSSL_NO_ECH + +# define OSSL_ECH_KEYGEN_MODE 0 /* default: generate a key pair/ECHConfig */ +# define OSSL_ECH_SELPRINT_MODE 1 /* we can print/down-select ECHConfigList */ +# define OSSL_ECH_MAXINFILES 5 /* we'll only take this many inputs */ + +typedef enum OPTION_choice { + /* standard openssl options */ + OPT_ERR = -1, OPT_EOF = 0, OPT_HELP, OPT_VERBOSE, OPT_TEXT, + OPT_OUT, OPT_IN, + /* ECHConfig specifics */ + OPT_PUBLICNAME, OPT_ECHVERSION, + OPT_MAXNAMELENGTH, OPT_HPKESUITE, + OPT_SELECT +} OPTION_CHOICE; + +const OPTIONS ech_options[] = { + OPT_SECTION("General options"), + {"help", OPT_HELP, '-', "Display this summary"}, + {"verbose", OPT_VERBOSE, '-', "Provide additional output"}, + {"text", OPT_TEXT, '-', "Provide human-readable output"}, + OPT_SECTION("Key generation"), + {"out", OPT_OUT, '>', + "Private key and/or ECHConfig [default: echconfig.pem]"}, + {"public_name", OPT_PUBLICNAME, 's', "public_name value"}, + {"max_name_len", OPT_MAXNAMELENGTH, 'n', + "Maximum host name length value [default: 0]"}, + {"suite", OPT_HPKESUITE, 's', "HPKE ciphersuite: e.g. \"0x20,1,3\""}, + {"ech_version", OPT_ECHVERSION, 'n', + "ECHConfig version [default: 0xff0d (13)]"}, + OPT_SECTION("ECH PEM file downselect/display"), + {"in", OPT_IN, '<', "An ECH PEM file"}, + {"select", OPT_SELECT, 'n', "Downselect to the numbered ECH config"}, + {NULL} +}; + +/** + * @brief map version string like 0xff01 or 65291 to uint16_t + * @param arg is the version string, from command line + * @return is the uint16_t value (with zero for error cases) + */ +static uint16_t verstr2us(char *arg) +{ + long lv = strtol(arg, NULL, 0); + uint16_t rv = 0; + + if (lv < 0xffff && lv > 0) + rv = (uint16_t)lv; + return rv; +} + +int ech_main(int argc, char **argv) +{ + char *prog = NULL; + OPTION_CHOICE o; + int i, rv = 1, verbose = 0, text = 0, outsupp = 0; + int select = OSSL_ECHSTORE_ALL, numinfiles = 0; + char *outfile = NULL, *infile = NULL; + char *infiles[OSSL_ECH_MAXINFILES] = { NULL }; + char *public_name = NULL, *suitestr = NULL; + uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; + uint8_t max_name_length = 0; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + int mode = OSSL_ECH_KEYGEN_MODE; /* key generation */ + OSSL_ECHSTORE *es = NULL; + BIO *ecf = NULL; + + prog = opt_init(argc, argv, ech_options); + while ((o = opt_next()) != OPT_EOF) { + switch (o) { + case OPT_EOF: + case OPT_ERR: + BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); + goto end; + case OPT_HELP: + opt_help(ech_options); + rv = 0; + goto end; + case OPT_VERBOSE: + verbose = 1; + break; + case OPT_TEXT: + text = 1; + break; + case OPT_SELECT: + mode = OSSL_ECH_SELPRINT_MODE; + select = strtol(opt_arg(), NULL, 10); + break; + case OPT_OUT: + outfile = opt_arg(); + outsupp = 1; + break; + case OPT_IN: + mode = OSSL_ECH_SELPRINT_MODE; + infile = opt_arg(); + if (numinfiles >= OSSL_ECH_MAXINFILES) { + BIO_printf(bio_err, "too many input files, only %d allowed\n", + OSSL_ECH_MAXINFILES); + goto opthelp; + } + infiles[numinfiles] = infile; + numinfiles++; + break; + case OPT_PUBLICNAME: + public_name = opt_arg(); + break; + case OPT_ECHVERSION: + ech_version = verstr2us(opt_arg()); + break; + case OPT_MAXNAMELENGTH: + { + long tmp = strtol(opt_arg(), NULL, 10); + + if (tmp < 0 || tmp > OSSL_ECH_MAX_MAXNAMELEN) { + BIO_printf(bio_err, + "max name length out of range [0,%d] (%ld)\n", + OSSL_ECH_MAX_MAXNAMELEN, tmp); + goto opthelp; + } else { + max_name_length = (uint8_t)tmp; + } + } + break; + case OPT_HPKESUITE: + suitestr = opt_arg(); + break; + } + } + argc = opt_num_rest(); + argv = opt_rest(); + if (argc != 0) { + BIO_printf(bio_err, "%s: Unknown parameter %s\n", prog, argv[0]); + goto opthelp; + } + /* Check ECH-specific inputs */ + switch (ech_version) { + case OSSL_ECH_RFCXXXX_VERSION: /* fall through */ + case 13: + ech_version = OSSL_ECH_RFCXXXX_VERSION; + break; + default: + BIO_printf(bio_err, "Un-supported version (0x%04x)\n", ech_version); + goto end; + } + if (max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { + BIO_printf(bio_err, "Weird max name length (0x%04x) - biggest is " + "(0x%04x) - exiting\n", max_name_length, + OSSL_ECH_MAX_MAXNAMELEN); + ERR_print_errors(bio_err); + goto end; + } + if (suitestr != NULL) { + if (OSSL_HPKE_str2suite(suitestr, &hpke_suite) != 1) { + BIO_printf(bio_err, "Bad OSSL_HPKE_SUITE (%s)\n", suitestr); + ERR_print_errors(bio_err); + goto end; + } + } + /* Set default if needed */ + if (outfile == NULL) + outfile = "echconfig.pem"; + es = OSSL_ECHSTORE_new(NULL, NULL); + if (es == NULL) + goto end; + if (mode == OSSL_ECH_KEYGEN_MODE) { + if (verbose) + BIO_printf(bio_err, "Calling OSSL_ECHSTORE_new_config\n"); + if ((ecf = BIO_new_file(outfile, "w")) == NULL + || OSSL_ECHSTORE_new_config(es, ech_version, max_name_length, + public_name, hpke_suite) != 1 + || OSSL_ECHSTORE_write_pem(es, 0, ecf) != 1) { + BIO_printf(bio_err, "OSSL_ECHSTORE_new_config error\n"); + goto end; + } + if (verbose) + BIO_printf(bio_err, "OSSL_ECHSTORE_new_config success\n"); + rv = 0; + } + if (mode == OSSL_ECH_SELPRINT_MODE) { + if (numinfiles == 0) + goto opthelp; + 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 */ + goto end; + } + BIO_free(ecf); + ecf = NULL; + } + if (verbose) + BIO_printf(bio_err, "Success reading %d files\n", numinfiles); + if (outsupp == 1) { + /* write result to that, with downselection if required */ + if (verbose) + BIO_printf(bio_err, "Will write to %s\n", outfile); + if (verbose && select != OSSL_ECHSTORE_ALL) + 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"); + goto end; + } + if (verbose) + BIO_printf(bio_err, "Success writing to %s\n", outfile); + } + rv = 0; + } + if (text) { + int oi_ind, oi_cnt = 0; + + if (OSSL_ECHSTORE_num_entries(es, &oi_cnt) != 1) + goto end; + if (verbose) + BIO_printf(bio_err, "Printing %d ECHConfigList\n", oi_cnt); + for (oi_ind = 0; oi_ind != oi_cnt; oi_ind++) { + time_t secs = 0; + char *pn = NULL, *ec = NULL; + int has_priv, for_retry; + + if (OSSL_ECHSTORE_get1_info(es, oi_ind, &secs, &pn, &ec, + &has_priv, &for_retry) != 1) { + OPENSSL_free(pn); /* just in case */ + OPENSSL_free(ec); + goto end; + } + BIO_printf(bio_err, "ECH entry: %d public_name: %s age: %lld%s\n", + oi_ind, pn, (long long)secs, + has_priv ? " (has private key)" : ""); + BIO_printf(bio_err, "\t%s\n", ec); + OPENSSL_free(pn); + OPENSSL_free(ec); + } + if (verbose) + BIO_printf(bio_err, "Success printing %d ECHConfigList\n", oi_cnt); + rv = 0; + } +end: + OSSL_ECHSTORE_free(es); + BIO_free_all(ecf); + return rv; +opthelp: + BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); + BIO_printf(bio_err, "\tup to %d -in instances allowed\n", OSSL_ECH_MAXINFILES); + return rv; +} + +#endif diff --git a/apps/lib/s_cb.c b/apps/lib/s_cb.c index b567b179b685f..1741c6ccc15f1 100644 --- a/apps/lib/s_cb.c +++ b/apps/lib/s_cb.c @@ -788,6 +788,10 @@ static const STRINT_PAIR tlsext_types[] = { {"certificate authorities", TLSEXT_TYPE_certificate_authorities}, {"post handshake auth", TLSEXT_TYPE_post_handshake_auth}, {"early_data", TLSEXT_TYPE_early_data}, +#ifndef OPENSSL_NO_ECH + {"encrypted ClientHello (draft-13)", TLSEXT_TYPE_ech}, + {"outer exts", TLSEXT_TYPE_outer_extensions}, +#endif {NULL} }; diff --git a/apps/list.c b/apps/list.c index 2aeedbbda8959..373d9ce916113 100644 --- a/apps/list.c +++ b/apps/list.c @@ -1614,6 +1614,9 @@ static void list_disabled(void) #ifdef OPENSSL_NO_ZSTD BIO_puts(bio_out, "ZSTD\n"); #endif +#ifdef OPENSSL_NO_ECH + BIO_puts(bio_out, "ECH\n"); +#endif } /* Unified enum for help and list commands. */ diff --git a/apps/s_client.c b/apps/s_client.c index 7c3e1ef7bc8a1..9fcf533ed9d2c 100644 --- a/apps/s_client.c +++ b/apps/s_client.c @@ -107,6 +107,14 @@ static int keymatexportlen = 20; static BIO *bio_c_out = NULL; static int c_quiet = 0; static char *sess_out = NULL; +# ifndef OPENSSL_NO_ECH +static char *ech_config_list = NULL, *ech_grease_suite = NULL; +static const char *sni_outer_name = NULL; +static int ech_grease = 0, ech_ignore_cid = 0; +static int ech_select = OSSL_ECHSTORE_ALL; +static int ech_grease_type = OSSL_ECH_CURRENT_VERSION; +static int ech_no_outer_sni = 0; +# endif static SSL_SESSION *psksess = NULL; static void print_stuff(BIO *berr, SSL *con, int full); @@ -522,6 +530,12 @@ typedef enum OPTION_choice { OPT_ENABLE_CLIENT_RPK, OPT_SCTP_LABEL_BUG, OPT_KTLS, +# ifndef OPENSSL_NO_ECH + OPT_ECHCONFIGLIST, OPT_SNIOUTER, OPT_ALPN_OUTER, + OPT_ECH_SELECT, OPT_ECH_IGNORE_CONFIG_ID, + OPT_ECH_GREASE, OPT_ECH_GREASE_SUITE, OPT_ECH_GREASE_TYPE, + OPT_ECH_NO_OUTER_SNI, +# endif OPT_R_ENUM, OPT_PROV_ENUM } OPTION_CHOICE; @@ -726,6 +740,27 @@ const OPTIONS s_client_options[] = { {"use_srtp", OPT_USE_SRTP, 's', "Offer SRTP key management with a colon-separated profile list"}, #endif + +# ifndef OPENSSL_NO_ECH + {"ech_config_list", OPT_ECHCONFIGLIST, 's', + "Set ECHConfigList, value is base64-encoded ECHConfigList"}, + {"ech_outer_alpn", OPT_ALPN_OUTER, 's', + "Specify outer ALPN value, when using ECH (comma-separated list)"}, + {"ech_outer_sni", OPT_SNIOUTER, 's', + "The name to put in the outer CH when overriding the server's choice"}, + {"ech_no_outer_sni", OPT_ECH_NO_OUTER_SNI, '-', + "Do not send the server name (SNI) extension in the outer ClientHello"}, + {"ech_select", OPT_ECH_SELECT, 'n', + "Select one ECHConfig from the set provided via -ech_config_list"}, + {"ech_grease", OPT_ECH_GREASE, '-', + "Send GREASE values when not really using ECH"}, + {"ech_grease_suite", OPT_ECH_GREASE_SUITE, 's', + "Use this HPKE suite for GREASE values when not really using ECH"}, + {"ech_grease_type", OPT_ECH_GREASE_TYPE, 'n', + "Use this TLS extension type for GREASE values when not really using ECH"}, + {"ech_ignore_cid", OPT_ECH_IGNORE_CONFIG_ID, '-', + "Ignore the server-chosen ECH config ID and send a random value"}, +# endif #ifndef OPENSSL_NO_SRP {"srpuser", OPT_SRPUSER, 's', "(deprecated) SRP authentication for 'user'"}, {"srppass", OPT_SRPPASS, 's', "(deprecated) Password for 'user'"}, @@ -924,6 +959,11 @@ int s_client_main(int argc, char **argv) char *sname_alloc = NULL; int noservername = 0; const char *alpn_in = NULL; +# ifndef OPENSSL_NO_ECH + const char *alpn_outer_in = NULL; + int rv = 0; + OSSL_ECHSTORE *es = NULL; +# endif tlsextctx tlsextcbp = { NULL, 0 }; const char *ssl_config = NULL; #define MAX_SI_TYPES 100 @@ -1527,6 +1567,35 @@ int s_client_main(int argc, char **argv) case OPT_SERVERNAME: servername = opt_arg(); break; +# ifndef OPENSSL_NO_ECH + case OPT_ECHCONFIGLIST: + ech_config_list = opt_arg(); + break; + case OPT_ALPN_OUTER: + alpn_outer_in = opt_arg(); + break; + case OPT_SNIOUTER: + sni_outer_name = opt_arg(); + break; + case OPT_ECH_SELECT: + ech_select = atoi(opt_arg()); + break; + case OPT_ECH_GREASE: + ech_grease = 1; + break; + case OPT_ECH_GREASE_SUITE: + ech_grease_suite = opt_arg(); + break; + case OPT_ECH_GREASE_TYPE: + ech_grease_type = atoi(opt_arg()); + break; + case OPT_ECH_IGNORE_CONFIG_ID: + ech_ignore_cid = 1; + break; + case OPT_ECH_NO_OUTER_SNI: + ech_no_outer_sni = 1; + break; +# endif case OPT_NOSERVERNAME: noservername = 1; break; @@ -1639,7 +1708,16 @@ int s_client_main(int argc, char **argv) goto opthelp; } } - +# ifndef OPENSSL_NO_ECH + if ((alpn_outer_in != NULL || sni_outer_name != NULL + || ech_no_outer_sni == 1) + && ech_config_list == NULL) { + BIO_printf(bio_err, "%s: Can't use -ech_outer_sni nor " + "-ech_outer_alpn nor -no_ech_outer_sni without " + "-ech_config_list\n", prog); + goto opthelp; + } +# endif #ifndef OPENSSL_NO_NEXTPROTONEG if (min_version == TLS1_3_VERSION && next_proto_neg_in != NULL) { BIO_printf(bio_err, "Cannot supply -nextprotoneg with TLSv1.3\n"); @@ -1870,6 +1948,13 @@ int s_client_main(int argc, char **argv) SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS); #endif +# ifndef OPENSSL_NO_ECH + if (ech_grease != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE); + if (ech_ignore_cid != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_IGNORE_CID); +# endif + if (vpmtouched && !SSL_CTX_set1_param(ctx, vpm)) { BIO_printf(bio_err, "Error setting verify params\n"); goto end; @@ -2075,6 +2160,26 @@ int s_client_main(int argc, char **argv) if (set_keylog_file(ctx, keylog_file)) goto end; +# ifndef OPENSSL_NO_ECH + if (alpn_outer_in != NULL) { + size_t alpn_outer_len; + unsigned char *alpn_outer = NULL; + + alpn_outer = next_protos_parse(&alpn_outer_len, alpn_outer_in); + if (alpn_outer == NULL) { + BIO_printf(bio_err, "Error parsing -ech_outer_alpn argument\n"); + goto end; + } + if (SSL_CTX_ech_set1_outer_alpn_protos(ctx, alpn_outer, + alpn_outer_len) != 1) { + BIO_printf(bio_err, "Error setting ALPN-OUTER\n"); + OPENSSL_free(alpn_outer); + goto end; + } + OPENSSL_free(alpn_outer); + } +# endif + con = SSL_new(ctx); if (con == NULL) goto end; @@ -2094,6 +2199,25 @@ int s_client_main(int argc, char **argv) } } +# ifndef OPENSSL_NO_ECH + if (ech_grease_suite != NULL) { + if (SSL_ech_set1_grease_suite(con, ech_grease_suite) != 1) { + ERR_print_errors(bio_err); + goto end; + } + } + /* no point in setting to our default */ + if (ech_grease_type != OSSL_ECH_CURRENT_VERSION) { + BIO_printf(bio_err, "Setting GREASE ECH type 0x%4x\n", ech_grease_type); + if (SSL_ech_set_grease_type(con, ech_grease_type) != 1) { + BIO_printf(bio_err, "Can't set GREASE ECH type 0x%4x\n", + ech_grease_type); + ERR_print_errors(bio_err); + goto end; + } + } +# endif + if (sess_in != NULL) { SSL_SESSION *sess; BIO *stmp = BIO_new_file(sess_in, "r"); @@ -2108,6 +2232,7 @@ int s_client_main(int argc, char **argv) goto end; } if (!SSL_set_session(con, sess)) { + SSL_SESSION_free(sess); BIO_printf(bio_err, "Can't set session\n"); goto end; } @@ -2129,6 +2254,50 @@ int s_client_main(int argc, char **argv) } } +# ifndef OPENSSL_NO_ECH + if (ech_config_list != NULL) { + if (SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list, + strlen(ech_config_list)) != 1) { + BIO_printf(bio_err, "%s: error setting ECHConfigList.\n", prog); + goto end; + } + if (ech_no_outer_sni == 1) { + if (sni_outer_name != NULL) { + BIO_printf(bio_err, "%s: can't set -ech_no_outer_sni and " + "-ech_outer_sni together.\n", prog); + goto end; + } + if (SSL_ech_set1_outer_server_name(con, NULL, 1) != 1) { + BIO_printf(bio_err, "%s: setting no ECH outer name failed.\n", + prog); + ERR_print_errors(bio_err); + goto end; + } + } + if (sni_outer_name != NULL) { + rv = SSL_ech_set1_outer_server_name(con, sni_outer_name, 0); + if (rv != 1) { + BIO_printf(bio_err, "%s: setting ECH outer name to %s failed.\n", + prog, sni_outer_name); + ERR_print_errors(bio_err); + goto end; + } + } + } + if (ech_select != OSSL_ECHSTORE_ALL) { + if ((es = SSL_get1_echstore(con)) == NULL + || OSSL_ECHSTORE_downselect(es, ech_select) != 1 + || SSL_set1_echstore(con, es) != 1) { + BIO_printf(bio_err, "%s: ECH downselect to (%d) failed.\n", + prog, ech_select); + ERR_print_errors(bio_err); + goto end; + } + OSSL_ECHSTORE_free(es); + es = NULL; + } +# endif + if (dane_tlsa_domain != NULL) { if (SSL_dane_enable(con, dane_tlsa_domain) <= 0) { BIO_printf(bio_err, "%s: Error enabling DANE TLSA " @@ -3349,6 +3518,9 @@ int s_client_main(int argc, char **argv) bio_c_out = NULL; BIO_free(bio_c_msg); bio_c_msg = NULL; +# ifndef OPENSSL_NO_ECH + OSSL_ECHSTORE_free(es); +# endif return ret; } @@ -3391,6 +3563,104 @@ static void print_cert_key_info(BIO *bio, X509 *cert) OPENSSL_free(curve); } +# ifndef OPENSSL_NO_ECH +static void print_ech_retry_configs(BIO *bio, SSL *s) +{ + int ind, cnt = 0, has_priv, for_retry; + OSSL_ECHSTORE *es = NULL; + time_t secs = 0; + char *pn = NULL, *ec = NULL; + size_t rtlen = 0; + unsigned char *rtval = NULL; + BIO *biom = NULL; + + if (SSL_ech_get1_retry_config(s, &rtval, &rtlen) != 1) { + BIO_printf(bio, "ECH: Error getting retry-configs\n"); + return; + } + /* + * print nicely, note that any non-supported versions + * sent by server will have been filtered out by now + */ + if (rtlen > INT_MAX + || (biom = BIO_new(BIO_s_mem())) == NULL + || BIO_write(biom, rtval, (int)rtlen) <= 0 + || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL + || OSSL_ECHSTORE_read_echconfiglist(es, biom) != 1) { + BIO_printf(bio, "ECH: Error loading retry-configs\n"); + goto end; + } + if (OSSL_ECHSTORE_num_entries(es, &cnt) != 1) + goto end; + BIO_printf(bio, "ECH: Got %d retry-configs\n", cnt); + for (ind = 0; ind != cnt; ind++) { + if (OSSL_ECHSTORE_get1_info(es, ind, &secs, &pn, &ec, + &has_priv, &for_retry) != 1) { + BIO_printf(bio, "ECH: Error getting retry-config %d.\n", ind); + goto end; + } + BIO_printf(bio, "ECH: entry: %d public_name: %s age: %lld%s\n", + ind, pn, (long long)secs, + has_priv ? " (has private key)" : ""); + BIO_printf(bio, "ECH: \t%s\n", ec); + OPENSSL_free(pn); + pn = NULL; + OPENSSL_free(ec); + ec = NULL; + } +end: + BIO_free_all(biom); + OPENSSL_free(rtval); + OPENSSL_free(pn); + OPENSSL_free(ec); + OSSL_ECHSTORE_free(es); + return; +} + +/* outcomes marked as "odd" shouldn't happen in s_client */ +static void print_ech_status(BIO *bio, SSL *s, int estat) +{ + switch (estat) { + case SSL_ECH_STATUS_NOT_TRIED: + BIO_printf(bio, "ECH: not tried: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED: + BIO_printf(bio, "ECH: tried but failed: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED_ECH: + BIO_printf(bio, "ECH: failed+retry-configs: %d\n", estat); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(bio, "ECH: success: %d\n", estat); + break; + case SSL_ECH_STATUS_GREASE_ECH: + BIO_printf(bio, "ECH: GREASE+retry-configs: %d\n", estat); + break; + case SSL_ECH_STATUS_BACKEND: + BIO_printf(bio, "ECH: BACKEND: %d\n", estat); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(bio, "ECH: GREASE: %d\n", estat); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(bio, "ECH: BAD CALL: %d\n", estat); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_printf(bio, "ECH: BAD NAME: %d\n", estat); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(bio, "ECH: NOT CONFIGURED: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED_ECH_BAD_NAME: + BIO_printf(bio, "ECH: failed+retry-configs: %d\n", estat); + break; + default: + BIO_printf(bio, "ECH: unexpected status: %d\n", estat); + } + return; +} +# endif + static void print_stuff(BIO *bio, SSL *s, int full) { X509 *peer = NULL; @@ -3405,6 +3675,10 @@ static void print_stuff(BIO *bio, SSL *s, int full) #ifndef OPENSSL_NO_CT const SSL_CTX *ctx = SSL_get_SSL_CTX(s); #endif +# ifndef OPENSSL_NO_ECH + char *inner = NULL, *outer = NULL; + int estat = 0; +# endif if (full) { int got_a_chain = 0; @@ -3634,6 +3908,21 @@ static void print_stuff(BIO *bio, SSL *s, int full) OPENSSL_free(exportedkeymat); } BIO_printf(bio, "---\n"); +# ifndef OPENSSL_NO_ECH + estat = SSL_ech_get1_status(s, &inner, &outer); + print_ech_status(bio, s, estat); + if (estat == SSL_ECH_STATUS_SUCCESS) { + BIO_printf(bio, "ECH: inner: %s\n", inner); + BIO_printf(bio, "ECH: outer: %s\n", outer); + } + if (estat == SSL_ECH_STATUS_FAILED_ECH + || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME) + print_ech_retry_configs(bio, s); + OPENSSL_free(inner); + OPENSSL_free(outer); + BIO_printf(bio, "---\n"); +# endif + /* flush, or debugging output gets mixed with http response */ (void)BIO_flush(bio); } diff --git a/apps/s_server.c b/apps/s_server.c index 82590f9adbbd8..572787edde14c 100644 --- a/apps/s_server.c +++ b/apps/s_server.c @@ -18,6 +18,9 @@ #if defined(_WIN32) /* Included before async.h to avoid some warnings */ # include +# if !defined(OPENSSL_NO_ECH) && !defined(PATH_MAX) +# define PATH_MAX 4096 +# endif #endif #include @@ -26,6 +29,27 @@ #include #include "internal/sockets.h" /* for openssl_fdset() */ +#ifndef OPENSSL_NO_ECH +/* to use tracing, if configured and requested */ +# ifndef OPENSSL_NO_SSL_TRACE +# include +# endif +/* sockaddr stuff */ +# if defined(_WIN32) +# include +# include +# include +# else +# include +# include +# include +# include +# endif +/* for timing in some TRACE statements */ +# include +# include "internal/o_dir.h" /* for OPENSSL_DIR_read */ +#endif + #ifndef OPENSSL_NO_SOCK /* @@ -59,6 +83,11 @@ typedef unsigned int u_int; #include "internal/sockets.h" #include "internal/statem.h" +# ifndef OPENSSL_NO_ECH +/* needed for X509_check_host in some CI builds "no-http" */ +# include +# endif + static int not_resumable_sess_cb(SSL *s, int is_forward_secure); static int sv_body(int s, int stype, int prot, unsigned char *context); static int www_body(int s, int stype, int prot, unsigned char *context); @@ -72,6 +101,10 @@ static void init_session_cache_ctx(SSL_CTX *sctx); static void free_sessions(void); static void print_connection_info(SSL *con); +# ifndef OPENSSL_NO_ECH +static unsigned int ech_print_cb(SSL *s, const char *str); +# endif + static const int bufsize = 16 * 1024; static int accept_socket = -1; @@ -420,8 +453,194 @@ typedef struct tlsextctx_st { char *servername; BIO *biodebug; int extension_error; + X509 *scert; /* ECH needs 2nd cert for testing */ } tlsextctx; +# ifndef OPENSSL_NO_ECH +static unsigned int ech_print_cb(SSL *s, const char *str) +{ + if (str != NULL) + BIO_printf(bio_s_out, "ECH Server callback printing: \n%s\n", str); + return 1; +} + +/* + * The server has possibly 2 TLS server names basically in ctx and ctx2. So we + * need to check if any client-supplied SNI in the inner/outer matches either + * and serve whichever is appropriate. X509_check_host is the way to do that, + * given an X509* pointer. + * + * We default to the "main" ctx if the client-supplied SNI does not match the + * ctx2 certificate. We don't fail if the client-supplied SNI matches neither, + * but just continue with the "main" ctx. If the client-supplied SNI matches + * both ctx and ctx2, then we'll switch to ctx2 anyway - we don't try for a + * "best" match in that case. + * + * Note that since we attempt ECH decryption whenever configured to do that, + * the only way to get the "outer" SNI is via SSL_ech_get1_status. + */ + +/* apparently 26 is all we need, but round it up to 32 to be on the safe side */ +# define ECH_TIME_STR_LEN 32 + +static int ssl_ech_servername_cb(SSL *s, int *ad, void *arg) +{ + tlsextctx *p = (tlsextctx *) arg; + time_t now = time(0); /* For a bit of basic logging */ + int sockfd = 0, res = 0, echrv = 0; + size_t srv = 0; + struct sockaddr_storage ss; + socklen_t salen = sizeof(ss); + struct sockaddr *sa; + char clientip[INET6_ADDRSTRLEN], lstr[ECH_TIME_STR_LEN]; + const char *servername = NULL; + char *inner_sni = NULL, *outer_sni = NULL; + struct tm local; +# if !defined(OPENSSL_SYS_WINDOWS) + struct tm *local_p = NULL; +# else + errno_t grv; +# endif + +# if !defined(OPENSSL_SYS_WINDOWS) + local_p = gmtime_r(&now, &local); + if (local_p != &local) { + strcpy(lstr, "sometime"); + } else { + srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local); + if (srv == 0) + strcpy(lstr, "sometime"); + } +# else + grv = gmtime_s(&local, &now); + if (grv != 0) { + strcpy(lstr, "sometime"); + } else { + srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local); + if (srv == 0) + strcpy(lstr, "sometime"); + } +# endif + memset(clientip, 0, INET6_ADDRSTRLEN); + strncpy(clientip, "unknown", INET6_ADDRSTRLEN); + memset(&ss, 0, salen); + sa = (struct sockaddr *)&ss; + res = BIO_get_fd(SSL_get_wbio(s), &sockfd); + if (res != -1) { +# if !defined(_WIN32) + res = getpeername(sockfd, sa, &salen); +# else + res = getpeername(sockfd, sa, (int *)&salen); +# endif + if (res == 0) + res = getnameinfo(sa, salen, clientip, INET6_ADDRSTRLEN, + 0, 0, NI_NUMERICHOST); + } + /* Name that matches "main" ctx */ + servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); + echrv = SSL_ech_get1_status(s, &inner_sni, &outer_sni); + if (p->biodebug != NULL) { + /* spit out basic logging */ + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: connection from %s at %s\n", + clientip, lstr); + /* Client supplied SNI from inner and outer */ + switch (echrv) { + case SSL_ECH_STATUS_BACKEND: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ECH backend got inner ECH\n"); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ECH not configured\n"); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: attempt we think is GREASE\n"); + break; + case SSL_ECH_STATUS_NOT_TRIED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: not attempted\n"); + break; + case SSL_ECH_STATUS_FAILED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: tried but failed\n"); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: bad input to API\n"); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: worked but bad name\n"); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: success: outer %s, inner: %s\n", + (outer_sni == NULL ? "none" : outer_sni), + (inner_sni == NULL ? "none" : inner_sni)); + break; + default: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Error getting ECH status\n"); + break; + } + } + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + if (servername != NULL && p->biodebug != NULL) { + const char *cp = servername; + unsigned char uc; + + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Hostname in TLS extension: \""); + while ((uc = *cp++) != 0) + BIO_printf(p->biodebug, + isascii(uc) && isprint(uc) ? "%c" : "\\x%02x", uc); + BIO_printf(p->biodebug, "\"\n"); + if (p->servername != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ctx servername: %s\n", + p->servername); + else + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ctx servername is NULL\n"); + if (p->scert == NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: No 2nd cert! That's bad.\n"); + } + if (p->servername == NULL) + return SSL_TLSEXT_ERR_NOACK; + if (p->scert == NULL) + return SSL_TLSEXT_ERR_NOACK; + if (echrv == SSL_ECH_STATUS_SUCCESS && servername != NULL) { + if (ctx2 != NULL) { + int check_host = X509_check_host(p->scert, servername, 0, 0, NULL); + + if (check_host == 1) { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Switching context.\n"); + SSL_set_SSL_CTX(s, ctx2); + } else { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Not switching context " + "- no name match (%d).\n", check_host); + } + } + } else { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Not switching context " + "- no ECH SUCCESS\n"); + } + return SSL_TLSEXT_ERR_OK; +} +/* Below is the "original" ssl_servername_cb, before ECH */ + +# else + static int ssl_servername_cb(SSL *s, int *ad, void *arg) { tlsextctx *p = (tlsextctx *) arg; @@ -452,6 +671,8 @@ static int ssl_servername_cb(SSL *s, int *ad, void *arg) return SSL_TLSEXT_ERR_OK; } +# endif + /* Structure passed to cert status callback */ typedef struct tlsextstatusctx_st { int timeout; @@ -960,6 +1181,10 @@ typedef enum OPTION_choice { OPT_TFO, OPT_CERT_COMP, OPT_ENABLE_SERVER_RPK, OPT_ENABLE_CLIENT_RPK, +# ifndef OPENSSL_NO_ECH + OPT_ECH_PEM, OPT_ECH_DIR, OPT_ECH_NORETRY, + OPT_ECH_TRIALDECRYPT, OPT_ECH_GREASE_RT, +# endif OPT_R_ENUM, OPT_S_ENUM, OPT_V_ENUM, @@ -1209,6 +1434,19 @@ const OPTIONS s_server_options[] = { #endif {"alpn", OPT_ALPN, 's', "Set the advertised protocols for the ALPN extension (comma-separated list)"}, + +# ifndef OPENSSL_NO_ECH + {"ech_key", OPT_ECH_PEM, 's', "Load ECH PEM-formatted key pair"}, + {"ech_dir", OPT_ECH_DIR, 's', "Load ECH key pairs (for retries) " \ + "from the specified directory"}, + {"ech_noretry_dir", OPT_ECH_NORETRY, 's', "Load ECH key pairs (not " \ + "for retry) from the specified directory"}, + {"ech_trialdecrypt", OPT_ECH_TRIALDECRYPT, '-', + "Do trial decryption even if ECH record_digest matching fails"}, + {"ech_greaseretries", OPT_ECH_GREASE_RT, '-', + "Set server to GREASE retry_config values"}, +# endif + #ifndef OPENSSL_NO_KTLS {"ktls", OPT_KTLS, '-', "Enable Kernel TLS for sending and receiving"}, {"sendfile", OPT_SENDFILE, '-', "Use sendfile to response file with -WWW"}, @@ -1224,6 +1462,64 @@ const OPTIONS s_server_options[] = { {NULL} }; +# ifndef OPENSSL_NO_ECH +static int ech_load_dir(SSL_CTX *lctx, const char *thedir, + int for_retry, int *nloaded) +{ + size_t elen = strlen(thedir); + OPENSSL_DIR_CTX *d = NULL; + const char *thisfile = NULL; + OSSL_ECHSTORE *es = NULL; + BIO *in = NULL; + int loaded = 0; + + if ((elen + 7) >= PATH_MAX) { /* too long, go away */ + BIO_printf(bio_err, "'%s' too long - exiting\n", thedir); + return 0; + } + if (app_isdir(thedir) <= 0) { /* if not a directory, ignore it */ + BIO_printf(bio_err, "'%s' not a directory - exiting\n", thedir); + return 0; + } + if ((es = SSL_CTX_get1_echstore(lctx)) == NULL + && (es = OSSL_ECHSTORE_new(app_get0_libctx(), + app_get0_propq())) == NULL) { + BIO_printf(bio_err, "internal error\n"); + return 0; + } + while ((thisfile = OPENSSL_DIR_read(&d, thedir))) { + char filepath[PATH_MAX]; + int r; + +# ifdef OPENSSL_SYS_VMS + r = BIO_snprintf(filepath, sizeof(filepath), "%s%s", thedir, thisfile); +# else + r = BIO_snprintf(filepath, sizeof(filepath), "%s/%s", thedir, thisfile); +# endif + 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); + continue; + } + BIO_free_all(in); + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", thisfile); + loaded++; + } + if (SSL_CTX_set1_echstore(lctx, es) != 1) { + BIO_printf(bio_err, "internal error\n"); + return 0; + } + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added %d ECH key pairs from: %s\n", + loaded, thedir); + *nloaded = loaded; + return 1; +} +# endif + #define IS_PROT_FLAG(o) \ (o == OPT_SSL3 || o == OPT_TLS1 || o == OPT_TLS1_1 || o == OPT_TLS1_2 \ || o == OPT_TLS1_3 || o == OPT_DTLS || o == OPT_DTLS1 || o == OPT_DTLS1_2) @@ -1266,7 +1562,7 @@ int s_server_main(int argc, char *argv[]) OPTION_CHOICE o; EVP_PKEY *s_key2 = NULL; X509 *s_cert2 = NULL; - tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING }; + tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING, NULL }; const char *ssl_config = NULL; int read_buf_len = 0; #ifndef OPENSSL_NO_NEXTPROTONEG @@ -1304,6 +1600,14 @@ int s_server_main(int argc, char *argv[]) int max_early_data = -1, recv_max_early_data = -1; char *psksessf = NULL; int no_ca_names = 0; +# ifndef OPENSSL_NO_ECH + char *echkeyfile = NULL; + char *echkeydir = NULL; + char *echnoretrydir = NULL; + int ech_files_loaded = 0; + int echtrialdecrypt = 0; /* trial decryption off by default */ + int echgrease_rc = 0; /* retry_config GREASEing off by default */ +# endif #ifndef OPENSSL_NO_SCTP int sctp_label_bug = 0; #endif @@ -1904,6 +2208,23 @@ int s_server_main(int argc, char *argv[]) case OPT_HTTP_SERVER_BINMODE: http_server_binmode = 1; break; +# ifndef OPENSSL_NO_ECH + case OPT_ECH_PEM: + echkeyfile = opt_arg(); + break; + case OPT_ECH_DIR: + echkeydir = opt_arg(); + break; + case OPT_ECH_NORETRY: + echnoretrydir = opt_arg(); + break; + case OPT_ECH_TRIALDECRYPT: + echtrialdecrypt = 1; + break; + case OPT_ECH_GREASE_RT: + echgrease_rc = 1; + break; +# endif case OPT_NOCANAMES: no_ca_names = 1; break; @@ -2063,6 +2384,9 @@ int s_server_main(int argc, char *argv[]) if (s_cert2 == NULL) goto end; +# ifndef OPENSSL_NO_ECH + tlsextcbp.scert = s_cert2; +# endif } } #if !defined(OPENSSL_NO_NEXTPROTONEG) @@ -2275,12 +2599,69 @@ int s_server_main(int argc, char *argv[]) goto end; } +# ifndef OPENSSL_NO_ECH + if (echtrialdecrypt != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_TRIALDECRYPT); + if (echgrease_rc != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG); + if (echkeyfile != NULL) { + OSSL_ECHSTORE *es = NULL; + BIO *in = NULL; + + if ((in = BIO_new_file(echkeyfile, "r")) == NULL + || (es = OSSL_ECHSTORE_new(app_get0_libctx(), + app_get0_propq())) == 0 + || OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY) != 1 + || SSL_CTX_set1_echstore(ctx, es) != 1) { + BIO_printf(bio_err, "Failed reading: %s\n", echkeyfile); + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + goto end; + } + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", echkeyfile); + ech_files_loaded++; + } + if (echkeydir != NULL) { + int nloaded = 0; + + if (ech_load_dir(ctx, echkeydir, OSSL_ECH_FOR_RETRY, &nloaded) != 1) { + BIO_printf(bio_err, "error loading from %s\n", echkeydir); + goto end; + } + ech_files_loaded += nloaded; + } + if (echnoretrydir != NULL) { + int nloaded = 0; + + if (ech_load_dir(ctx, echnoretrydir, OSSL_ECH_NO_RETRY, + &nloaded) != 1) { + BIO_printf(bio_err, "error loading from %s\n", echnoretrydir); + goto end; + } + ech_files_loaded += nloaded; + } + if ((echkeyfile != NULL || echkeydir != NULL || echnoretrydir != NULL) + && bio_s_out != NULL) { + BIO_printf(bio_s_out, "Loaded %d ECH key pairs in total\n", + ech_files_loaded); + } +# endif + if (s_cert2) { ctx2 = SSL_CTX_new_ex(app_get0_libctx(), app_get0_propq(), meth); if (ctx2 == NULL) { ERR_print_errors(bio_err); goto end; } +# ifndef OPENSSL_NO_ECH + if (echtrialdecrypt != 0) + SSL_CTX_set_options(ctx2, SSL_OP_ECH_TRIALDECRYPT); + if (echgrease_rc != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG); +# endif } if (ctx2 != NULL) { @@ -2339,6 +2720,13 @@ int s_server_main(int argc, char *argv[]) if (alpn_ctx.data) SSL_CTX_set_alpn_select_cb(ctx, alpn_cb, &alpn_ctx); + /* + * If we have a 2nd context to which we might switch, then set + * the same alpn callback for that too. + */ + if (s_cert2 != NULL && alpn_ctx.data != NULL) + SSL_CTX_set_alpn_select_cb(ctx2, alpn_cb, &alpn_ctx); + if (!no_dhe) { EVP_PKEY *dhpkey = NULL; @@ -2413,9 +2801,21 @@ int s_server_main(int argc, char *argv[]) goto end; } +# ifndef OPENSSL_NO_ECH + /* + * Giving the same chain to the 2nd key pair works for our tests. + * It would be better to supply s_chain_file2 as a new CLA in case + * the paths are very different but as that's not needed for tests, + * I didn't do it. + */ + if (ctx2 != NULL + && !set_cert_key_stuff(ctx2, s_cert2, s_key2, s_chain, build_chain)) + goto end; +# else if (ctx2 != NULL && !set_cert_key_stuff(ctx2, s_cert2, s_key2, NULL, build_chain)) goto end; +# endif if (s_dcert != NULL) { if (!set_cert_key_stuff(ctx, s_dcert, s_dkey, s_dchain, build_chain)) @@ -2497,10 +2897,19 @@ int s_server_main(int argc, char *argv[]) goto end; } tlsextcbp.biodebug = bio_s_out; +# ifndef OPENSSL_NO_ECH + SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_ech_servername_cb); + SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp); + SSL_CTX_set_tlsext_servername_callback(ctx, ssl_ech_servername_cb); + SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp); + SSL_CTX_ech_set_callback(ctx2, ech_print_cb); + SSL_CTX_ech_set_callback(ctx, ech_print_cb); +# else SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp); SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp); +# endif } #ifndef OPENSSL_NO_SRP @@ -2528,6 +2937,11 @@ int s_server_main(int argc, char *argv[]) #endif if (set_keylog_file(ctx, keylog_file)) goto end; +# ifndef OPENSSL_NO_ECH + /* not really an ECH issue but needed */ + if (ctx2 != NULL && set_keylog_file(ctx2, keylog_file)) + goto end; +# endif if (max_early_data >= 0) SSL_CTX_set_max_early_data(ctx, max_early_data); @@ -3542,6 +3956,10 @@ static int www_body(int s, int stype, int prot, unsigned char *context) X509 *peer = NULL; STACK_OF(SSL_CIPHER) *sk; static const char *space = " "; +# ifndef OPENSSL_NO_ECH + char *ech_inner = NULL, *ech_outer = NULL; + int echrv = 0; +# endif if (www == 1 && HAS_PREFIX(buf, "GET /reneg")) { if (HAS_PREFIX(buf, "GET /renegcert")) @@ -3605,6 +4023,80 @@ static int www_body(int s, int stype, int prot, unsigned char *context) } BIO_puts(io, "\n"); +# ifndef OPENSSL_NO_ECH + /* Customise output a bit to show ECH info at top */ + BIO_puts(io, "

OpenSSL with ECH

\n"); + BIO_puts(io, "

\n"); + echrv = SSL_ech_get1_status(con, &ech_inner, &ech_outer); + switch (echrv) { + case SSL_ECH_STATUS_NOT_TRIED: + BIO_puts(io, "ECH not attempted\n"); + break; + case SSL_ECH_STATUS_FAILED: + BIO_puts(io, "ECH tried but failed\n"); + break; + case SSL_ECH_STATUS_FAILED_ECH: + BIO_puts(io, "ECH tried but we got ECH which is weird\n"); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_puts(io, "ECH worked but bad name\n"); + break; + case SSL_ECH_STATUS_BACKEND: + BIO_printf(io, "ECH acting as backend\n"); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(io, "ECH not configured\n"); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(io, "ECH attempt we interpret as GREASE\n"); + break; + case SSL_ECH_STATUS_GREASE_ECH: + BIO_printf(io, "ECH attempt we interpret as GREASE, + ECH\n"); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(io, "ECH bad input to API\n"); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(io, "ECH success: outer sni: %s, inner sni: %s\n", + (ech_outer == NULL ? "none" : ech_outer), + (ech_inner == NULL ? "none" : ech_inner)); + break; + default: + BIO_printf(io, " Error getting ECH status\n"); + break; + } + BIO_puts(io, "

\n"); + BIO_puts(io, "

TLS Session details

\n"); + BIO_puts(io, "
\n");
+            /*
+             * also dump session info to server stdout for debugging
+             */
+            SSL_SESSION_print(bio_s_out, SSL_get_session(con));
+            BIO_puts(io, "
\n");
+            BIO_puts(io, "\n");
+            for (i = 0; i < local_argc; i++) {
+                const char *myp;
+
+                for (myp = local_argv[i]; *myp; myp++)
+                    switch (*myp) {
+                    case '<':
+                        BIO_puts(io, "<");
+                        break;
+                    case '>':
+                        BIO_puts(io, ">");
+                        break;
+                    case '&':
+                        BIO_puts(io, "&");
+                        break;
+                    default:
+                        BIO_write(io, myp, 1);
+                        break;
+                    }
+                BIO_write(io, " ", 1);
+            }
+            BIO_puts(io, "\n");
+# endif
+
             ssl_print_secure_renegotiation_notes(io, con);
 
             /*
diff --git a/crypto/err/openssl.txt b/crypto/err/openssl.txt
index d8333369722f2..1e80022159029 100644
--- a/crypto/err/openssl.txt
+++ b/crypto/err/openssl.txt
@@ -1373,6 +1373,7 @@ SSL_R_BAD_DH_VALUE:102:bad dh value
 SSL_R_BAD_DIGEST_LENGTH:111:bad digest length
 SSL_R_BAD_EARLY_DATA:233:bad early data
 SSL_R_BAD_ECC_CERT:304:bad ecc cert
+SSL_R_BAD_ECHCONFIG_EXTENSION:425:bad echconfig extension
 SSL_R_BAD_ECPOINT:306:bad ecpoint
 SSL_R_BAD_EXTENSION:110:bad extension
 SSL_R_BAD_HANDSHAKE_LENGTH:332:bad handshake length
@@ -1453,6 +1454,8 @@ SSL_R_DTLS_MESSAGE_TOO_BIG:334:dtls message too big
 SSL_R_DUPLICATE_COMPRESSION_ID:309:duplicate compression id
 SSL_R_ECC_CERT_NOT_FOR_SIGNING:318:ecc cert not for signing
 SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE:374:ecdh required for suiteb mode
+SSL_R_ECH_DECODE_ERROR:426:ech decode error
+SSL_R_ECH_REQUIRED:424:ech required
 SSL_R_EE_KEY_TOO_SMALL:399:ee key too small
 SSL_R_EMPTY_RAW_PUBLIC_KEY:349:empty raw public key
 SSL_R_EMPTY_SRTP_PROTECTION_PROFILE_LIST:354:empty srtp protection profile list
diff --git a/crypto/ssl_err.c b/crypto/ssl_err.c
index b791daf5489a9..fd15792aa935e 100644
--- a/crypto/ssl_err.c
+++ b/crypto/ssl_err.c
@@ -37,6 +37,8 @@ static const ERR_STRING_DATA SSL_str_reasons[] = {
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_DIGEST_LENGTH), "bad digest length"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_EARLY_DATA), "bad early data"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECC_CERT), "bad ecc cert"},
+    {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECHCONFIG_EXTENSION),
+     "bad echconfig extension"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECPOINT), "bad ecpoint"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_EXTENSION), "bad extension"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_HANDSHAKE_LENGTH),
@@ -156,6 +158,8 @@ static const ERR_STRING_DATA SSL_str_reasons[] = {
      "ecc cert not for signing"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE),
      "ecdh required for suiteb mode"},
+    {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECH_DECODE_ERROR), "ech decode error"},
+    {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECH_REQUIRED), "ech required"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EE_KEY_TOO_SMALL), "ee key too small"},
     {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EMPTY_RAW_PUBLIC_KEY),
      "empty raw public key"},
diff --git a/demos/sslecho/Makefile b/demos/sslecho/Makefile
index defb1597e1c76..79b0efe697207 100644
--- a/demos/sslecho/Makefile
+++ b/demos/sslecho/Makefile
@@ -4,7 +4,7 @@
 #
 #    LD_LIBRARY_PATH=../.. ./sslecho
 
-TESTS = sslecho
+TESTS = sslecho echecho
 
 CFLAGS  = -I../../include -g -Wall
 LDFLAGS = -L../..
@@ -14,6 +14,8 @@ all: $(TESTS)
 
 sslecho: main.o
 
+echecho: echecho.o
+
 $(TESTS):
 	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS)
 
diff --git a/demos/sslecho/README.md b/demos/sslecho/README.md
index 58f7ca07245b4..8ef3e93b7484d 100644
--- a/demos/sslecho/README.md
+++ b/demos/sslecho/README.md
@@ -24,3 +24,48 @@ The cert.pem and key.pem files included are self signed certificates with the
 "Common Name" of 'localhost'.
 
 Best to create the 'pem' files using an actual hostname.
+
+Encrypted Client Hello (ECH) Variant
+====================================
+
+``echecho.c`` implements the same functionality but demonstrates minimal code
+changes needed to use ECH. The ``echecho`` binary has the same user interface
+discussed above but enables ECH for the connection, based on hard-coded ECH
+configuration data. A real server would load file(s), and a real client would
+acquire an ECHConfigList from the DNS.
+
+All that's required to use ECH is to load ECH data via `OSSL_ECHSTORE_read_*`
+APIs and then enable ECH via ``SSL_CTX_set1_echstore()``. Both client and
+server check and print out the status of ECH using ``SSL_ech_get1_status()``,
+but that's optional.
+
+To run the server:
+
+            $ LD_LIBRARY_PATH=../.. ./echecho s
+
+To run the client:
+
+            $ LD_LIBRARY_PATH=../.. ./echecho c localhost
+
+All going well both server and client will print the ECH status at the
+start of each connection. That looks like:
+
+            ECH worked (status: 1, inner: localhost, outer: example.com)
+
+If the non-ECH demo client (``sslecho``) is used instead the server will
+output:
+
+            ECH failed/not-tried (status: -101, inner: (null), outer: (null))
+
+If the non-ECH demo server (i.e., ``sslecho``) is used, the client will exit
+with an error as ECH was attempted and failed. In a debug build, that looks
+like:
+
+            80EBEE54227F0000:error:0A000163:SSL routines:tls_process_initial_server_flight:ech required:ssl/statem/statem_clnt.c:3274:
+
+A real client would likely fall back to not using ECH, but the above
+is ok for a demo.
+
+In that case, the server will also exit based on the ECH alert from the client:
+
+            403787A8307F0000:error:0A000461:SSL routines:ssl3_read_bytes:reason(1121):../ssl/record/rec_layer_s3.c:1588:SSL alert number 121
diff --git a/demos/sslecho/echecho.c b/demos/sslecho/echecho.c
new file mode 100644
index 0000000000000..36f6845de4b50
--- /dev/null
+++ b/demos/sslecho/echecho.c
@@ -0,0 +1,405 @@
+/*
+ *  Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ *  Licensed under the Apache License 2.0 (the "License").  You may not use
+ *  this file except in compliance with the License.  You can obtain a copy
+ *  in the file LICENSE in the source distribution or at
+ *  https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+static const int server_port = 4433;
+
+static const char echconfig[] = "AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEAAQALZXhhbXBsZS5jb20AAA==";
+static const char echprivbuf[] =
+    "-----BEGIN PRIVATE KEY-----\n"
+    "MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V\n"
+    "-----END PRIVATE KEY-----\n"
+    "-----BEGIN ECHCONFIG-----\n"
+    "AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEAAQALZXhhbXBsZS5jb20AAA==\n"
+    "-----END ECHCONFIG-----\n";
+
+typedef unsigned char   bool;
+#define true            1
+#define false           0
+
+/*
+ * This flag won't be useful until both accept/read (TCP & SSL) methods
+ * can be called with a timeout. TBD.
+ */
+static volatile bool    server_running = true;
+
+int create_socket(bool isServer)
+{
+    int s;
+    int optval = 1;
+    struct sockaddr_in addr = { 0 };
+
+    s = socket(AF_INET, SOCK_STREAM, 0);
+    if (s < 0) {
+        perror("Unable to create socket");
+        exit(EXIT_FAILURE);
+    }
+
+    if (isServer) {
+        addr.sin_family = AF_INET;
+        addr.sin_port = htons(server_port);
+        addr.sin_addr.s_addr = INADDR_ANY;
+
+        /* Reuse the address; good for quick restarts */
+        if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
+                < 0) {
+            perror("setsockopt(SO_REUSEADDR) failed");
+            exit(EXIT_FAILURE);
+        }
+
+        if (bind(s, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
+            perror("Unable to bind");
+            exit(EXIT_FAILURE);
+        }
+
+        if (listen(s, 1) < 0) {
+            perror("Unable to listen");
+            exit(EXIT_FAILURE);
+        }
+    }
+
+    return s;
+}
+
+SSL_CTX* create_context(bool isServer)
+{
+    const SSL_METHOD *method;
+    SSL_CTX *ctx;
+
+    if (isServer)
+        method = TLS_server_method();
+    else
+        method = TLS_client_method();
+
+    ctx = SSL_CTX_new(method);
+    if (ctx == NULL) {
+        perror("Unable to create SSL context");
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+
+    return ctx;
+}
+
+static int configure_ech(SSL_CTX *ctx, int server,
+                         unsigned char *buf, size_t len)
+{
+    OSSL_ECHSTORE *es = NULL;
+    BIO *es_in = BIO_new_mem_buf(buf, len);
+
+    if (es_in == NULL || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL)
+        goto err;
+    if (server && OSSL_ECHSTORE_read_pem(es, es_in, 1) != 1)
+        goto err;
+    if (!server && OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1)
+        goto err;
+    if (SSL_CTX_set1_echstore(ctx, es) != 1)
+        goto err;
+    BIO_free_all(es_in);
+    return 1;
+err:
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(es_in);
+    return 0;
+}
+
+void configure_server_context(SSL_CTX *ctx)
+{
+    /* Set the key and cert */
+    if (SSL_CTX_use_certificate_chain_file(ctx, "cert.pem") <= 0) {
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+
+    if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) {
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+
+    if (configure_ech(ctx, 1, (unsigned char*)echprivbuf,
+                      sizeof(echprivbuf) - 1) != 1) {
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+}
+
+void configure_client_context(SSL_CTX *ctx)
+{
+    /*
+     * Configure the client to abort the handshake if certificate verification
+     * fails
+     */
+    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
+    /*
+     * In a real application you would probably just use the default system certificate trust store and call:
+     *     SSL_CTX_set_default_verify_paths(ctx);
+     * In this demo though we are using a self-signed certificate, so the client must trust it directly.
+     */
+    if (!SSL_CTX_load_verify_locations(ctx, "cert.pem", NULL)) {
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+    if (configure_ech(ctx, 0, (unsigned char*)echconfig,
+                      sizeof(echconfig) - 1) != 1) {
+        ERR_print_errors_fp(stderr);
+        exit(EXIT_FAILURE);
+    }
+}
+
+void usage()
+{
+    printf("Usage: echecho s\n");
+    printf("       --or--\n");
+    printf("       echecho c ip\n");
+    printf("       c=client, s=server, ip=dotted ip of server\n");
+    exit(1);
+}
+
+int main(int argc, char **argv)
+{
+    bool isServer;
+    int result;
+
+    SSL_CTX *ssl_ctx = NULL;
+    SSL *ssl = NULL;
+
+    int server_skt = -1;
+    int client_skt = -1;
+
+    /* used by getline relying on realloc, can't be statically allocated */
+    char *txbuf = NULL;
+    size_t txcap = 0;
+    int txlen;
+
+    char rxbuf[128];
+    size_t rxcap = sizeof(rxbuf);
+    int rxlen;
+
+    char *rem_server_ip = NULL;
+
+    struct sockaddr_in addr = { 0 };
+    unsigned int addr_len = sizeof(addr);
+
+    char *outer_sni = NULL, *inner_sni = NULL;
+    int ech_status;
+
+    /* Splash */
+    printf("\nechecho : Simple Echo Client/Server: %s : %s\n\n", __DATE__,
+    __TIME__);
+
+    /* Need to know if client or server */
+    if (argc < 2) {
+        usage();
+        /* NOTREACHED */
+    }
+    isServer = (argv[1][0] == 's') ? true : false;
+    /* If client get remote server address (could be 127.0.0.1) */
+    if (!isServer) {
+        if (argc != 3) {
+            usage();
+            /* NOTREACHED */
+        }
+        rem_server_ip = argv[2];
+    }
+
+    /* Create context used by both client and server */
+    ssl_ctx = create_context(isServer);
+
+    /* If server */
+    if (isServer) {
+
+        printf("We are the server on port: %d\n\n", server_port);
+
+        /* Configure server context with appropriate key files */
+        configure_server_context(ssl_ctx);
+
+        /* Create server socket; will bind with server port and listen */
+        server_skt = create_socket(true);
+
+        /*
+         * Loop to accept clients.
+         * Need to implement timeouts on TCP & SSL connect/read functions
+         * before we can catch a CTRL-C and kill the server.
+         */
+        while (server_running) {
+            /* Wait for TCP connection from client */
+            client_skt = accept(server_skt, (struct sockaddr*) &addr,
+                                &addr_len);
+            if (client_skt < 0) {
+                perror("Unable to accept");
+                exit(EXIT_FAILURE);
+            }
+
+            printf("Client TCP connection accepted\n");
+
+            /* Create server SSL structure using newly accepted client socket */
+            ssl = SSL_new(ssl_ctx);
+            SSL_set_fd(ssl, client_skt);
+
+            /* Wait for SSL connection from the client */
+            if (SSL_accept(ssl) <= 0) {
+                ERR_print_errors_fp(stderr);
+                server_running = false;
+            } else {
+
+                printf("Client SSL connection accepted\n\n");
+
+                ech_status = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni);
+                printf("ECH %s (status: %d, inner: %s, outer: %s)\n",
+                        (ech_status == 1 ? "worked" : "failed/not-tried"),
+                        ech_status, inner_sni, outer_sni);
+                OPENSSL_free(inner_sni);
+                OPENSSL_free(outer_sni);
+                inner_sni = outer_sni = NULL;
+
+                /* Echo loop */
+                while (true) {
+                    /* Get message from client; will fail if client closes connection */
+                    if ((rxlen = SSL_read(ssl, rxbuf, rxcap)) <= 0) {
+                        if (rxlen == 0) {
+                            printf("Client closed connection\n");
+                        }
+                        ERR_print_errors_fp(stderr);
+                        break;
+                    }
+                    /* Insure null terminated input */
+                    rxbuf[rxlen] = 0;
+                    /* Look for kill switch */
+                    if (strcmp(rxbuf, "kill\n") == 0) {
+                        /* Terminate...with extreme prejudice */
+                        printf("Server received 'kill' command\n");
+                        server_running = false;
+                        break;
+                    }
+                    /* Show received message */
+                    printf("Received: %s", rxbuf);
+                    /* Echo it back */
+                    if (SSL_write(ssl, rxbuf, rxlen) <= 0) {
+                        ERR_print_errors_fp(stderr);
+                    }
+                }
+            }
+            if (server_running) {
+                /* Cleanup for next client */
+                SSL_shutdown(ssl);
+                SSL_free(ssl);
+                close(client_skt);
+            }
+        }
+        printf("Server exiting...\n");
+    }
+    /* Else client */
+    else {
+
+        printf("We are the client\n\n");
+
+        /* Configure client context so we verify the server correctly */
+        configure_client_context(ssl_ctx);
+
+        /* Create "bare" socket */
+        client_skt = create_socket(false);
+        /* Set up connect address */
+        addr.sin_family = AF_INET;
+        inet_pton(AF_INET, rem_server_ip, &addr.sin_addr.s_addr);
+        addr.sin_port = htons(server_port);
+        /* Do TCP connect with server */
+        if (connect(client_skt, (struct sockaddr*) &addr, sizeof(addr)) != 0) {
+            perror("Unable to TCP connect to server");
+            goto exit;
+        } else {
+            printf("TCP connection to server successful\n");
+        }
+
+        /* Create client SSL structure using dedicated client socket */
+        ssl = SSL_new(ssl_ctx);
+        SSL_set_fd(ssl, client_skt);
+        /* Set hostname for SNI */
+        SSL_set_tlsext_host_name(ssl, rem_server_ip);
+        /* Configure server hostname check */
+        SSL_set1_host(ssl, rem_server_ip);
+
+        /* Now do SSL connect with server */
+        if (SSL_connect(ssl) == 1) {
+
+            printf("SSL connection to server successful\n\n");
+
+            ech_status = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni);
+            printf("ECH %s (status: %d, inner: %s, outer: %s)\n",
+                    (ech_status == 1 ? "worked" : "failed/not-tried"),
+                    ech_status, inner_sni, outer_sni);
+            OPENSSL_free(inner_sni);
+            OPENSSL_free(outer_sni);
+            inner_sni = outer_sni = NULL;
+
+            /* Loop to send input from keyboard */
+            while (true) {
+                /* Get a line of input */
+                txlen = getline(&txbuf, &txcap, stdin);
+                /* Exit loop on error */
+                if (txlen < 0 || txbuf == NULL) {
+                    break;
+                }
+                /* Exit loop if just a carriage return */
+                if (txbuf[0] == '\n') {
+                    break;
+                }
+                /* Send it to the server */
+                if ((result = SSL_write(ssl, txbuf, txlen)) <= 0) {
+                    printf("Server closed connection\n");
+                    ERR_print_errors_fp(stderr);
+                    break;
+                }
+
+                /* Wait for the echo */
+                rxlen = SSL_read(ssl, rxbuf, rxcap);
+                if (rxlen <= 0) {
+                    printf("Server closed connection\n");
+                    ERR_print_errors_fp(stderr);
+                    break;
+                } else {
+                    /* Show it */
+                    rxbuf[rxlen] = 0;
+                    printf("Received: %s", rxbuf);
+                }
+            }
+            printf("Client exiting...\n");
+        } else {
+
+            printf("SSL connection to server failed\n\n");
+
+            ERR_print_errors_fp(stderr);
+        }
+    }
+    exit:
+    /* Close up */
+    if (ssl != NULL) {
+        SSL_shutdown(ssl);
+        SSL_free(ssl);
+    }
+    SSL_CTX_free(ssl_ctx);
+
+    if (client_skt != -1)
+        close(client_skt);
+    if (server_skt != -1)
+        close(server_skt);
+
+    if (txbuf != NULL && txcap > 0)
+        free(txbuf);
+
+    printf("echecho exiting\n");
+
+    return 0;
+}
diff --git a/doc/build.info b/doc/build.info
index 36aa5b08bb5bc..013c483f03343 100644
--- a/doc/build.info
+++ b/doc/build.info
@@ -88,6 +88,12 @@ DEPEND[man/man1/openssl-ec.1]=man1/openssl-ec.pod
 GENERATE[man/man1/openssl-ec.1]=man1/openssl-ec.pod
 DEPEND[man1/openssl-ec.pod]{pod}=man1/openssl-ec.pod.in
 GENERATE[man1/openssl-ec.pod]=man1/openssl-ec.pod.in
+DEPEND[html/man1/openssl-ech.html]=man1/openssl-ech.pod
+GENERATE[html/man1/openssl-ech.html]=man1/openssl-ech.pod
+DEPEND[man/man1/openssl-ech.1]=man1/openssl-ech.pod
+GENERATE[man/man1/openssl-ech.1]=man1/openssl-ech.pod
+DEPEND[man1/openssl-ech.pod]{pod}=man1/openssl-ech.pod.in
+GENERATE[man1/openssl-ech.pod]=man1/openssl-ech.pod.in
 DEPEND[html/man1/openssl-ecparam.html]=man1/openssl-ecparam.pod
 GENERATE[html/man1/openssl-ecparam.html]=man1/openssl-ecparam.pod
 DEPEND[man/man1/openssl-ecparam.1]=man1/openssl-ecparam.pod
@@ -374,6 +380,7 @@ html/man1/openssl-dhparam.html \
 html/man1/openssl-dsa.html \
 html/man1/openssl-dsaparam.html \
 html/man1/openssl-ec.html \
+html/man1/openssl-ech.html \
 html/man1/openssl-ecparam.html \
 html/man1/openssl-enc.html \
 html/man1/openssl-engine.html \
@@ -436,6 +443,7 @@ man/man1/openssl-dhparam.1 \
 man/man1/openssl-dsa.1 \
 man/man1/openssl-dsaparam.1 \
 man/man1/openssl-ec.1 \
+man/man1/openssl-ech.1 \
 man/man1/openssl-ecparam.1 \
 man/man1/openssl-enc.1 \
 man/man1/openssl-engine.1 \
@@ -2791,6 +2799,10 @@ DEPEND[html/man3/SSL_session_reused.html]=man3/SSL_session_reused.pod
 GENERATE[html/man3/SSL_session_reused.html]=man3/SSL_session_reused.pod
 DEPEND[man/man3/SSL_session_reused.3]=man3/SSL_session_reused.pod
 GENERATE[man/man3/SSL_session_reused.3]=man3/SSL_session_reused.pod
+DEPEND[html/man3/SSL_set1_echstore.html]=man3/SSL_set1_echstore.pod
+GENERATE[html/man3/SSL_set1_echstore.html]=man3/SSL_set1_echstore.pod
+DEPEND[man/man3/SSL_set1_echstore.3]=man3/SSL_set1_echstore.pod
+GENERATE[man/man3/SSL_set1_echstore.3]=man3/SSL_set1_echstore.pod
 DEPEND[html/man3/SSL_set1_host.html]=man3/SSL_set1_host.pod
 GENERATE[html/man3/SSL_set1_host.html]=man3/SSL_set1_host.pod
 DEPEND[man/man3/SSL_set1_host.3]=man3/SSL_set1_host.pod
@@ -3757,6 +3769,7 @@ html/man3/SSL_read.html \
 html/man3/SSL_read_early_data.html \
 html/man3/SSL_rstate_string.html \
 html/man3/SSL_session_reused.html \
+html/man3/SSL_set1_echstore.html \
 html/man3/SSL_set1_host.html \
 html/man3/SSL_set1_initial_peer_addr.html \
 html/man3/SSL_set1_server_cert_type.html \
@@ -4431,6 +4444,7 @@ man/man3/SSL_read.3 \
 man/man3/SSL_read_early_data.3 \
 man/man3/SSL_rstate_string.3 \
 man/man3/SSL_session_reused.3 \
+man/man3/SSL_set1_echstore.3 \
 man/man3/SSL_set1_host.3 \
 man/man3/SSL_set1_initial_peer_addr.3 \
 man/man3/SSL_set1_server_cert_type.3 \
diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md
new file mode 100644
index 0000000000000..e9808035141e5
--- /dev/null
+++ b/doc/designs/ech-api.md
@@ -0,0 +1,645 @@
+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 `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.
+
+The ECH Protocol
+----------------
+
+ECH involves creating an "inner" ClientHello (CH) that contains the potentially
+sensitive content of a CH, primarily the SNI and perhaps the ALPN values. That
+inner CH is then encrypted and embedded (as a CH extension) in an outer CH that
+contains presumably less sensitive values. The spec includes a "compression"
+scheme that allows the inner CH to refer to extensions from the outer CH where
+the same value would otherwise be present in both.
+
+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 descriptions here are less formal and provide some justification for the
+API design.
+
+Unless otherwise stated all APIs return 1 in the case of success and 0 for
+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.
+
+General Approach
+----------------
+
+This ECH implementation has been prototyped via integrations with curl, apache2,
+lighttpd, nginx 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.
+
+To date, 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 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/).
+
+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
+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.)
+
+```c
+/* version from RFC XXXX */
+#  define OSSL_ECH_RFCXXXX_VERSION 0xfe0d
+/* latest version from an RFC */
+#  define OSSL_ECH_CURRENT_VERSION OSSL_ECH_RFCXXXX_VERSION
+```
+
+Note that 0xfe0d is also the value of the ECH extension codepoint:
+
+```c
+#  define TLSEXT_TYPE_ech                       0xfe0d
+```
+
+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
+-------------------
+
+TODO(ECH): This sample code has only been compiled. The `OSSL_ECHSTORE` stuff
+doesn't work yet.
+
+OpenSSL includes code for an [`sslecho`](../../demos/sslecho) demo.  We've
+added a minimal [`echecho`](../../demos/sslecho/echecho.c) that shows how to
+ECH-enable this demo.
+
+Handling Custom Extensions
+--------------------------
+
+OpenSSL supports custom extensions (via `SSL_CTX_add_custom_ext()`) so that
+extension values are supplied and parsed by client and server applications via
+a callback.  The ECH specification of course doesn't deal with such
+implementation matters, but comprehensive ECH support for such custom
+extensions could quickly become complex. At present, in the absence of evidence
+of sensitive custom extension values, we handle all such extensions by using
+the ECH compression mechanism.  That means we require no API changes, only make
+one call to the application callbacks and get interoperability, but that such
+extension values remain visible to network observers. That could change if some
+custom value turns out to be sensitive such that we'd prefer to not include it
+in the outer CH.
+
+Padding
+-------
+
+The privacy protection provided by ECH benefits from an observer not being able
+to differentiate access to different web origins based on TLS handshake
+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
+`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).
+
+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)
+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.
+
+When running the ECH protocol, implementations are required to skip over
+unknown ECHConfig extensions, or to fail for so-called "mandatory" unsupported
+ECHConfig extensions. Our library code is compliant in that respect - it will
+skip over extensions that are not "mandatory" (extension type high bit clear)
+and fail if any "mandatory" ECHConfig extension (extension type high bit set)
+is seen.
+
+For testing purposes, ECHConfigList values that contain ECHConfig extensions
+can be produced using external scripts, and used with the library, but there is
+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
+utility. (If new fields for an ECHConfig are required, a new ECHConfig version
+with the proposed changes can 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
+ECHConfigList values.
+
+ECH keys versus TLS server keys
+-------------------------------
+
+ECH private keys are similar to, but different from, TLS server private keys
+used to authenticate servers. Notably:
+
+- ECH private keys are expected to be rotated roughly hourly, rather than every
+  month or two for TLS server private keys. Hourly ECH key rotation is an
+  attempt to provide better forward secrecy, given ECH implements an
+  ephemeral-static ECDH scheme.
+
+- ECH private keys stand alone - there are no hierarchies and there is no
+  chaining, and no certificates and no defined relationships between current
+  and older ECH private keys. The expectation is that a "current" ECH public key
+  will be published in the DNS and that plus approx. 2 "older" ECH private keys
+  will remain usable for decryption at any given time. This is a way to balance
+  DNS TTLs versus forward secrecy and robustness.
+
+- 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.
+
+- 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
+  seen as FIPS compliance for ECH is not (yet) a thing. For OpenSSL, it seems
+  wise to be agnostic and support all relevant combinations. (And doing so is not
+  that hard.)
+
+ECH Store APIs
+--------------
+
+We introduce an externally opaque type `OSSL_ECHSTORE` to allow applications
+to create and manage ECHConfigList values and associated meta-data. The
+external APIs using `OSSL_ECHSTORE` are:
+
+```c
+typedef struct ossl_echstore_st OSSL_ECHSTORE;
+
+/* if a caller wants to index the last entry in the store */
+# define OSSL_ECHSTORE_LAST -1
+/* if a caller wants all entries in the store, e.g. to print public values */
+#  define OSSL_ECHSTORE_ALL -2
+
+OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq);
+void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es);
+int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
+                             uint16_t echversion, uint8_t max_name_length,
+                             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_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);
+int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry);
+int OSSL_ECHSTORE_num_entries(OSSL_ECHSTORE *es, int *numentries);
+int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys);
+int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age);
+```
+
+`OSSL_ECHSTORE_new()` and `OSSL_ECHSTORE_free()` are relatively obvious.
+
+`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/))
+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.)
+These two APIs will typically be used via the `openssl ech` command line tool.
+
+`OSSL_ECHSTORE_read_echconfiglist()` will typically be used by a client to
+ingest the "ech=" SvcParamKey value found in an SVCB or HTTPS RR retrieved from
+the DNS. The resulting set of ECHConfig values can then be associated with an
+`SSL_CTX` or `SSL` structure for TLS connections.
+
+`OSSL_ECHSTORE_get1_info()` presents the caller with information about the
+content of the store for logging or for display, e.g. in a command line tool.
+`OSSL_ECHSTORE_downselect()` API gives the client a way to select one
+particular ECHConfig value from the set stored (discarding the rest).
+
+`OSSL_ECHSTORE_set1_key_and_read_pem()` and `OSSL_ECHSTORE_read_pem()` can be
+used to load a private key value and associated "singleton" ECHConfigList.
+Those can be used (by servers) to enable ECH for an `SSL_CTX` or `SSL`
+connection. In addition to loading those values, the application can also
+indicate via `for_retry` which ECHConfig value(s) are to be included in the
+`retry_configs` fallback scheme defined by the ECH protocol.
+
+`OSSL_ECHSTORE_num_entries()` and `OSSL_ECHSTORE_num_keys()` allow an
+application  to see how many usable ECH configs and private keys are currently
+in the store, and `OSSL_ECHSTORE_flush_keys()` allows a server to flush keys
+that are older than `age` seconds.  The general model is that a server can
+maintain an `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.
+
+The APIs the clients and servers can use to associate an `OSSL_ECHSTORE`
+with an `SSL_CTX` or `SSL` structure:
+
+```c
+int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es);
+int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es);
+```
+
+ECH will be enabled for the relevant `SSL_CTX` or `SSL` connection
+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.
+
+```c
+int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len);
+```
+
+To access the `OSSL_ECHSTORE` associated with an `SSL_CTX` or
+`SSL` connection:
+
+```c
+OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx);
+OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s);
+```
+
+The resulting `OSSL_ECHSTORE` can be modified and then re-associated
+with an `SSL_CTX` or `SSL` connection.
+
+ECH Store Internals
+-------------------
+
+The internal structure of an ECH Store is as described below:
+
+```c
+typedef struct ossl_echext_st {
+    uint16_t type;
+    uint16_t len;
+    unsigned char *val;
+} OSSL_ECHEXT;
+
+DEFINE_STACK_OF(OSSL_ECHEXT)
+
+typedef struct ossl_echstore_entry_st {
+    uint16_t version; /* 0xff0d for draft-13 */
+    char *public_name;
+    size_t pub_len;
+    unsigned char *pub;
+    unsigned int nsuites;
+    OSSL_HPKE_SUITE *suites;
+    uint8_t max_name_length;
+    uint8_t config_id;
+    STACK_OF(OSSL_ECHEXT) *exts;
+    time_t loadtime; /* time public and private key were loaded from file */
+    EVP_PKEY *keyshare; /* long(ish) term ECH private keyshare on a server */
+    int for_retry; /* whether to use this ECHConfigList in a retry */
+    size_t encoded_len; /* length of overall encoded content */
+    unsigned char *encoded; /* overall encoded content */
+} OSSL_ECHSTORE_ENTRY;
+
+DEFINE_STACK_OF(OSSL_ECHSTORE_ENTRY)
+
+struct ossl_echstore_st {
+    STACK_OF(OSSL_ECHSTORE_ENTRY) *entries;
+    OSSL_LIB_CTX *libctx;
+    const char *propq;
+};
+```
+
+Some notes on the above ECHConfig fields:
+
+- `version` should be `OSSL_ECH_CURRENT_VERSION` for the current version.
+
+- `public_name` field is the name used in the SNI of the outer ClientHello, and
+  that a server ought be able to authenticate if using the `retry_configs`
+  fallback mechanism.
+
+- `config_id` is a one-octet value used by servers to select which private
+  value to use to attempt ECH decryption. Servers can also do trial decryption
+  if desired, as clients might use a random value for the `confid_id` as an
+  anti-fingerprinting mechanism. (The use of one octet for this value was the
+  result of an extended debate about efficiency versus fingerprinting.)
+
+- The `max_name_length` is an element of the ECHConfigList that is used by
+  clients as part of a padding algorithm. (That design is part of the spec, but
+  isn't necessarily great - the idea is to include the longest value that might
+  be the length of a DNS name included as an inner CH SNI.) A value of 0 is
+  perhaps most likely to be used, indicating that the maximum isn't known.
+
+Essentially, an ECH store is a set of ECHConfig values, plus optionally
+(for servers), relevant private key value information.
+
+When a non-singleton ECHConfigList is ingested, that is expanded into
+a store that is the same as if a set of singleton ECHConfigList values
+had been ingested sequentially.
+
+In addition to the obvious fields from each ECHConfig, we also store:
+
+- The `encoded` value (and length) of the ECHConfig, as that is used
+  as an input for the HPKE encapsulation of the inner ClientHello. (Used
+  by both clients and servers.)
+
+- 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.
+
+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.
+
+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
+the actual TLS session with the client, based on the inner CH content. The
+function to support this simply takes the outer CH, indicates whether
+decryption has succeeded or not, and if it has, returns the inner CH and SNI
+values (allowing routing to the correct back-end). Both the supplied (outer)
+CH and returned (inner) CH here include the record layer header.
+
+```c
+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);
+```
+
+The caller allocates the `inner_ch` buffer, on input `inner_len` should
+contain the size of the `inner_ch` buffer, on output the size of the actual
+inner CH. Note that, when ECH decryption succeeds, the inner CH will always be
+smaller than the outer CH.
+
+If there is no ECH present in the outer CH then this will return 1 (i.e., the
+call will succeed) but `decrypted_ok` will be zero. The same will result if a
+GREASEd ECH is present or decryption fails for some other (indistinguishable)
+reason.
+
+If the caller wishes to support HelloRetryRequest (HRR), then it must supply
+the same `hrrtok` and `toklen` pointers to both calls to
+`SSL_CTX_ech_raw_decrypt()` (for the initial and second ClientHello
+messages). When done, the caller must free the `hrrtok` using
+`OPENSSL_free()`.  If the caller doesn't need to support HRR, then it can
+supply NULL values for these parameters. The value of the token is the client's
+ephemeral public value, which is not sensitive having being sent in clear in
+the first ClientHello.  This value is missing from the second ClientHello but
+is needed for ECH decryption.
+
+Note that `SSL_CTX_ech_raw_decrypt()` only takes a ClientHello as input. If
+the flight containing the ClientHello contains other messages (e.g. a
+ChangeCipherSuite or Early data), then the caller is responsible for
+disentangling those, and for assembling a new flight containing the inner
+ClientHello.
+
+Different encodings
+-------------------
+
+ECHConfigList values may be provided via a command line argument to the calling
+application or (more likely) have been retrieved from DNS resource records by
+the application. ECHConfigList values may be provided in various encodings
+(base64 or binary) each of which may suit different applications.
+
+If the input contains more than one (syntactically correct) ECHConfigList, then only
+those that contain locally supported options (e.g. AEAD ciphers) will be
+returned. If no ECHConfigList found has supported options then none will be
+returned and the function will return NULL.
+
+Additional Client Controls
+--------------------------
+
+Clients can additionally more directly control the values to be used for inner
+and outer SNI and ALPN values via specific APIs. This allows a client to
+override the `public_name` present in an ECHConfigList that will otherwise
+be used for the outer SNI. The `no_outer` input allows a client to emit an
+outer CH with no SNI at all. Providing a `NULL` for the `outer_name` means
+to send the `public_name` provided from the ECHConfigList.
+
+```c
+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);
+int SSL_ech_set1_outer_alpn_protos(SSL *s, const unsigned char *protos,
+                                   size_t protos_len);
+int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos,
+                                       size_t protos_len);
+```
+
+If a client attempts ECH but that fails, or sends an ECH-GREASEd CH, to
+an ECH-supporting server, then that server may return an ECH "retry-config"
+value that the client could choose to use in a subsequent connection. The
+client can detect this situation via the `SSL_ech_get1_status()` API and
+can access the retry config value via:
+
+```c
+OSSL_ECHSTORE *SSL_ech_get1_retry_config(SSL *s);
+```
+
+GREASEing
+---------
+
+"GREASEing" is defined in
+[RFC8701](https://datatracker.ietf.org/doc/html/rfc8701) and is a mechanism
+intended to discourage protocol ossification that can be used for ECH.  GREASEd
+ECH may turn out to be important as an initial step towards widespread
+deployment of ECH.
+
+If a client wishes to GREASE ECH using a specific HPKE suite or ECH version
+(represented by the TLS extension type code-point) then it can set those values
+via:
+
+```c
+int SSL_ech_set1_grease_suite(SSL *s, const char *suite);
+int SSL_ech_set_grease_type(SSL *s, uint16_t type);
+```
+
+ECH Status API
+--------------
+
+Clients and servers can check the status of ECH processing
+on an SSL connection using this API:
+
+```c
+int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni);
+
+/* Return codes from SSL_ech_get1_status */
+#  define SSL_ECH_STATUS_BACKEND    4 /* ECH back-end: saw an ech_is_inner */
+#  define SSL_ECH_STATUS_GREASE_ECH 3 /* GREASEd and got an ECH in return */
+#  define SSL_ECH_STATUS_GREASE     2 /* ECH GREASE happened  */
+#  define SSL_ECH_STATUS_SUCCESS    1 /* Success */
+#  define SSL_ECH_STATUS_FAILED     0 /* Some internal or protocol error */
+#  define SSL_ECH_STATUS_BAD_CALL   -100 /* Some in/out arguments were NULL */
+#  define SSL_ECH_STATUS_NOT_TRIED  -101 /* ECH wasn't attempted  */
+#  define SSL_ECH_STATUS_BAD_NAME   -102 /* ECH ok but server cert bad */
+#  define SSL_ECH_STATUS_NOT_CONFIGURED -103 /* ECH wasn't configured */
+#  define SSL_ECH_STATUS_FAILED_ECH -105 /* We tried, failed and got an ECH, from a good name */
+#  define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* We tried, failed and got an ECH, from a bad name */
+```
+
+The `inner_sni` and `outer_sni` values should be freed by callers
+via `OPENSSL_free()`.
+
+The function returns one of the status values above.
+
+Call-backs and options
+----------------------
+
+Clients and servers can set a callback that will be triggered when ECH is
+attempted and the result of ECH processing is known. The callback function can
+access a string (`str`) that can be used for logging (but not for branching).
+Callback functions might typically call `SSL_ech_get1_status()` if branching
+is required.
+
+```c
+typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str);
+
+void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f);
+void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f);
+```
+
+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)
+```
+
+Build Options
+-------------
+
+All ECH code is protected via `#ifndef OPENSSL_NO_ECH` and there is
+a `no-ech` option to build without this code.
+
+BoringSSL APIs
+--------------
+
+Brief descriptions of BoringSSL APIs are below together with initial comments
+comparing those to the above. (It may be useful to consider the extent to
+which it is useful to make OpenSSL and BoringSSL APIs resemble one another.)
+
+Just as our implementation is under development, BoringSSL's
+`include/openssl/ssl.h` says: "ECH support in BoringSSL is still experimental
+and under development."
+
+### GREASE
+
+BoringSSL uses an API to enable GREASEing rather than an option.
+
+```c
+OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable);
+```
+
+This could work as well for our implementation, or BoringSSL could probably
+change to use an option, unless there's some reason to prefer not adding new
+options.
+
+### Verifying the outer CH rather than inner
+
+BoringSSL seems to use this API to change the DNS name being verified in order
+to validate a `retry_config`.
+
+```c
+OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl,
+                                               const char **out_name,
+                                               size_t *out_name_len);
+```
+
+I'm not sure how this compares. Need to investigate.
+
+### Create an ECHConfigList
+
+The first function below outputs an ECHConfig, the second adds one of those to
+an `SSL_ECH_KEYS` structure, the last emits an ECHConfigList from that
+structure. There are other APIs for managing memory for `SSL_ECH_KEYS`
+
+These APIs also expose HPKE to the application via `EVP_HPKE_KEY` which is
+defined in `include/openssl/hpke.h`. HPKE handling differs quite a bit from
+the HPKE APIs merged to OpenSSL.
+
+```c
+OPENSSL_EXPORT int SSL_marshal_ech_config(uint8_t **out, size_t *out_len,
+                                          uint8_t config_id,
+                                          const EVP_HPKE_KEY *key,
+                                          const char *public_name,
+                                          size_t max_name_len);
+OPENSSL_EXPORT int SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config,
+                                    const uint8_t *ech_config,
+                                    size_t ech_config_len,
+                                    const EVP_HPKE_KEY *key);
+OPENSSL_EXPORT int SSL_ECH_KEYS_marshal_retry_configs(const SSL_ECH_KEYS *keys,
+                                                      uint8_t **out,
+                                                      size_t *out_len);
+```
+
+Collectively these are similar to `OSSL_ECH_make_echconfig()`.
+
+### Setting ECH keys on a server
+
+Again using the `SSL_ECH_KEYS` type and APIs, servers can build up a set of
+ECH keys using:
+
+```c
+OPENSSL_EXPORT int SSL_CTX_set1_ech_keys(SSL_CTX *ctx, SSL_ECH_KEYS *keys);
+```
+
+### 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()`.
diff --git a/doc/man1/build.info b/doc/man1/build.info
index 5c41a1687dd45..4a998af5348df 100644
--- a/doc/man1/build.info
+++ b/doc/man1/build.info
@@ -17,6 +17,7 @@ DEPEND[openssl-dsaparam.pod]=../perlvars.pm
 DEPEND[openssl-dsa.pod]=../perlvars.pm
 DEPEND[openssl-ecparam.pod]=../perlvars.pm
 DEPEND[openssl-ec.pod]=../perlvars.pm
+DEPEND[openssl-ech.pod]=../perlvars.pm
 DEPEND[openssl-enc.pod]=../perlvars.pm
 DEPEND[openssl-engine.pod]=../perlvars.pm
 DEPEND[openssl-errstr.pod]=../perlvars.pm
diff --git a/doc/man1/openssl-ech.pod.in b/doc/man1/openssl-ech.pod.in
new file mode 100644
index 0000000000000..5abe0ae5b0d80
--- /dev/null
+++ b/doc/man1/openssl-ech.pod.in
@@ -0,0 +1,117 @@
+=pod
+{- OpenSSL::safe::output_do_not_edit_headers(); -}
+
+=head1 NAME
+
+openssl-ech - ECH key generation
+
+=head1 SYNOPSIS
+
+B B
+[B<-help>]
+[B<-verbose>]
+[B<-in> I]
+[B<-out> I]
+[B<-public_name> I]
+[B<-max_name_len> I]
+[B<-suite> I]
+[B<-ech_version> I]
+[B<-select> I]
+[B<-text>]
+
+=head1 DESCRIPTION
+
+The L command generates Encrypted Client Hello (ECH) key pairs
+in the ECHConfig PEM file format as specified in
+L.
+TODO(ECH): update I-D reference to RFC when possible.
+
+That format consists of an optional private key in PKCS#8 format and a base64
+encoded ECHConfigList containing an entry with a matching public value (and
+possibly other entries as well).
+
+=head1 OPTIONS
+
+The following options are supported:
+
+=over 4
+
+=item B<-help>
+
+Print out a usage message.
+
+=item B<-verbose>
+
+Print more verbosely.
+
+=item B<-in>
+
+Provide an input ECH PEM file for printing or merging. Up to five
+input files can be provided via use of multiple B arguments.
+
+=item B<-out> I
+
+Name of output ECHConfig PEM file.  If a new key pair was generated the output
+file will contain the private key and encoded ECHConfigList.  If one or more
+input files was provided the output file will contain a set of ECHConfigList
+values with public keys from the inputs, and no private key(s).
+
+=item B<-text>
+
+Provide human-readable text output.
+
+=item B<-public_name> I
+
+The DNS name to use in the "public_name" field of the ECHConfig.
+
+=item B<-max_name_len> I
+
+Maximum name length field value to use in the ECHConfig.
+
+=item B<-suite> I
+
+HPKE suite to use in the ECHConfig.
+
+=item B<-ech_version> I
+
+The ECH version to use in the ECHConfig. Only 0xfe0d is supported in this version.
+
+=item B<-select> I
+
+Select the N-th ECHConfig/public key from the set of input ECH PEM files and output
+that.
+
+=back
+
+=head1 NOTES
+
+Ciphersuites are specified using a comma-separated list of IANA-registered
+codes/numbers e.g. "-c 0x20,1,3" or a comma-separated list of strings from:
+- KEMs: p256, p384, p521, x25519, x448
+- KDFs: hkdf-sha256, hkdf-sha384, hkdf-sha512
+- AEADs: aes128gcm, aes256gcm, chachapoly1305
+
+For example the default is: x25519, hkdf-sha256, aes128gcm
+See L for details.
+
+=head1 SEE ALSO
+
+L,
+L,
+L,
+L
+
+=head1 HISTORY
+
+This functionality described here was added in OpenSSL 3.5.
+
+=head1 COPYRIGHT
+
+Copyright 2024 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
+L.
+
+=cut
diff --git a/doc/man1/openssl-s_client.pod.in b/doc/man1/openssl-s_client.pod.in
index 4c0759ab0f582..8de5efb7e18d1 100644
--- a/doc/man1/openssl-s_client.pod.in
+++ b/doc/man1/openssl-s_client.pod.in
@@ -124,6 +124,15 @@ B B
 [B<-enable_server_rpk>]
 [B<-enable_client_rpk>]
 [I:I]
+[B<-ech_config_list>]
+[B<-ech_outer_alpn> I]
+[B<-ech_grease>]
+[B<-ech_grease_suite> I]
+[B<-ech_grease_type> I]
+[B<-ech_ignore_cid>]
+[B<-ech_outer_sni> I]
+[B<-ech_no_outer_sni>]
+[B<-ech_select> I]
 
 =head1 DESCRIPTION
 
@@ -178,6 +187,15 @@ specified with this flag and issues an HTTP CONNECT command to connect
 to the desired server.
 If the host string is an IPv6 address, it must be enclosed in C<[> and C<]>.
 
+=item B<-ech_config_list> I
+
+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)
+=for comment TODO(ECH): replace XXXX when RFC published.
+
 =item B<-proxy_user> I
 
 When used with the B<-proxy> flag, the program will attempt to authenticate
@@ -822,6 +840,63 @@ nor B<-connect> are provided, falls back to attempting to connect to
 I on port I<4433>.
 If the host string is an IPv6 address, it must be enclosed in C<[> and C<]>.
 
+=item B<-ech_outer_alpn> I
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify
+ALPN values to use in the outer ClientHello. (A "normal" ALPN value
+specified via -alpn will be used in the inner ClientHello.)
+
+=item B<-ech_grease>
+
+When not really doing Encrypted Client Hello (ECH), one can emit a so-called
+GREASE value, which is essentially a random value in order to try ensure that
+server code is less likely to ossify.
+
+=item B<-ech_grease_suite> I
+
+When B<-ech_grease> is specified, one can choose which ECH ciphersuite to use
+via this parameter.
+
+The comma-separated suite string names an HPKE suite in the form of
+I,I,I, e.g. "x25519,hkdf-sha256,aes256gcm" or can use
+the numeric values (in decimal or hexadecimal form) from the HPKE specification
+so "0x20,0x01,0x02" is the same as the previous example.
+
+KEM values supported: p256 or 0x10; p384 or 0x11, p521 or 0x12, x25519 or 0x20, x448 or 0x21
+
+KDF values supported: hkdf-sha256 or 0x01, hkdf-sha384 or 0x02, hkdf-sha512 or 0x03
+
+AEAD values supported: aes128gcm or 0x01, aes256gcm or 0x02, chachapoly1305 or 0x03
+
+=item B<-ech_grease_type> I
+
+Allows the client to set the TLS extension type for a GREASEd ECH value
+(currently equivalent to the ECH version number).  The current default is
+0xfe0d.
+
+=item B<-ech_ignore_cid>
+
+Encrypted Client Hello (ECH) extensions contain a configuration identifier
+(cid) taken from the ECHConfigList usually found in the domain name system
+(DNS). As those identifiers could be revealing, the client has the option to
+use a random value instead.
+
+=item B<-ech_outer_sni> I
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify a
+subject name indication (SNI) value to use in the outer ClientHello over-riding
+the public_name value from the relevant ECHConfigList.
+
+=item B<-ech_no_outer_sni>
+
+Setting this flag means no SNI will be emitted in the outer ClientHello.
+
+=item B<-ech_select> I
+
+If an ECHConfigList contains more than one ECHConfig then the client will by
+default use the first that works. This allows the caller to specify which
+ECHConfig to use (using a zero-based index).
+
 =back
 
 =head1 CONNECTED COMMANDS (BASIC)
@@ -1048,6 +1123,8 @@ The
 and B<-ocsp_check_all>
 options were added in OpenSSL 3.6.
 
+The B options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/doc/man1/openssl-s_server.pod.in b/doc/man1/openssl-s_server.pod.in
index 4c30c9c628330..c3bb6d3bf3982 100644
--- a/doc/man1/openssl-s_server.pod.in
+++ b/doc/man1/openssl-s_server.pod.in
@@ -135,6 +135,11 @@ B B
 {- $OpenSSL::safe::opt_engine_synopsis -}{- $OpenSSL::safe::opt_provider_synopsis -}
 [B<-enable_server_rpk>]
 [B<-enable_client_rpk>]
+[B<-ech_key> I]
+[B<-ech_dir> I]
+[B<-ech_noretry_dir> I]
+[B<-ech_trialdecrypt>]
+[B<-ech_greaseretries>]
 
 =head1 DESCRIPTION
 
@@ -824,6 +829,32 @@ certificates can still elect to send X.509 certificates as usual.
 
 Raw public keys are extracted from the configured certificate/private key.
 
+=item B<-ech_key> I
+
+Load one Encrypted Client Hello (ECH) key pair.
+
+=item B<-ech_dir> I
+
+Attempt to load an ECH key pair from every file in the named directory.
+Any keys successfully loaded will be returned in 'retry_configs'.
+
+=item B<-ech_noretry_dir> I
+
+Attempt to load an ECH key pair from every file in the named directory.
+Keys loaded will not be returned in 'retry_configs'.
+
+=item B<-ech_trialdecrypt>
+
+When an Encrypted Client Hello (ECH) extension is seen in a ClientHello,
+attempt to decrypt with all known ECH private keys if necessary. Without
+this, the ECH "config_id" is used to match against the loaded ECH private
+keys and decryption is only attempted when there's a match.
+
+=item B<-ech_greaseretries>
+
+If set, servers will add GREASEy ECHConfig values to those sent
+in retry_configs.
+
 =back
 
 =head1 CONNECTED COMMANDS
@@ -938,6 +969,8 @@ options were added in OpenSSL 3.2.
 
 The B<-status_all> option was added in OpenSSL 3.6.
 
+The B options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/doc/man1/openssl.pod b/doc/man1/openssl.pod
index edef2ff598948..9fc416d3cbcee 100644
--- a/doc/man1/openssl.pod
+++ b/doc/man1/openssl.pod
@@ -123,6 +123,10 @@ L and L.
 
 EC (Elliptic curve) key processing.
 
+=item B
+
+Encrypted Client Hello (ECH) admin. See L.
+
 =item B
 
 EC parameter manipulation and generation.
diff --git a/doc/man3/SSL_CTX_set_client_hello_cb.pod b/doc/man3/SSL_CTX_set_client_hello_cb.pod
index 6367c68a62502..8f129ce7fa7f1 100644
--- a/doc/man3/SSL_CTX_set_client_hello_cb.pod
+++ b/doc/man3/SSL_CTX_set_client_hello_cb.pod
@@ -111,6 +111,8 @@ The SSL_client_hello_get0_*() functions return raw ClientHello data, whereas
 SSL_client_hello_get1_extensions_present() returns only recognized extensions
 (so unknown/GREASE-extensions are not included).
 
+TODO(ECH): How ECH is handled here needs to be documented.
+
 =head1 RETURN VALUES
 
 The application's supplied ClientHello callback returns
diff --git a/doc/man3/SSL_CTX_set_options.pod b/doc/man3/SSL_CTX_set_options.pod
index d78bdd5a31038..97bb373c18675 100644
--- a/doc/man3/SSL_CTX_set_options.pod
+++ b/doc/man3/SSL_CTX_set_options.pod
@@ -370,6 +370,44 @@ only understands up to SSLv3. In this case the client must still use the
 same SSLv3.1=TLSv1 announcement. Some clients step down to SSLv3 with respect
 to the server's answer and violate the version rollback protection.)
 
+=item SSL_OP_ECH_GREASE
+
+If set, TLS ClientHello messages emitted by the client will include GREASE
+Encrypted ClientHello (ECH) extension values, if ECH is not really being
+attempted.
+
+=item SSL_OP_ECH_TRIALDECRYPT
+
+If set, servers will attempt to decrypt ECH extensions using all loaded
+ECH key pairs. By default, servers will only attempt decryption using
+an ECH key pair that matches the config_id in the ECH extension value
+received.
+
+Note that a server that has loaded many ECH configurations and that enables ECH
+trial decryption will attempt decryption with every ECH key when presented with
+a GREASEd ECH, (see B) and with possibly that many even
+when presented with a real ECH.  That could easily become an accidental denial
+of service.
+
+Note also that the ECH specification recommends that servers that enable this
+option consider implementing some form of rate limiting mechanism to limit the
+potential damage caused in such scenarios.
+
+If trial decryption is enabled then decryption will be attempted with the ECH
+configurations in the order they were loaded. So, where it is possible to load
+the configuration most likely to be used first, that would improve efficiency.
+
+=item SSL_OP_ECH_GREASE_RETRY_CONFIG
+
+If set, servers will add GREASEy ECHConfig values to those sent to the
+client after the client GREASEd or the client tried and failed to use
+ECH.
+
+=item SSL_OP_ECH_IGNORED_CID
+
+If set, TLS ClientHello messages emitted by the client will ignore the
+ECHConfig config_id chosen by the server and use a random octet.
+
 =back
 
 The following options no longer have any effect but their identifiers are
diff --git a/doc/man3/SSL_set1_echstore.pod b/doc/man3/SSL_set1_echstore.pod
new file mode 100644
index 0000000000000..05034d3102b94
--- /dev/null
+++ b/doc/man3/SSL_set1_echstore.pod
@@ -0,0 +1,201 @@
+=pod
+
+=head1 NAME
+
+SSL_set1_echstore,
+OSSL_ECHSTORE_new, OSSL_ECHSTORE_free,
+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_entries,
+OSSL_ECHSTORE_num_keys, OSSL_ECHSTORE_flush_keys,
+SSL_CTX_set1_echstore,
+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_set_callback,SSL_set1_ech_config_list
+- Encrypted Client Hello (ECH) functions
+
+=head1 SYNOPSIS
+
+ #include 
+
+  OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq);
+  void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es);
+  int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
+                               uint16_t echversion, uint16_t max_name_length,
+                               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_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);
+  int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry);
+  int OSSL_ECHSTORE_num_entries(OSSL_ECHSTORE *es, int *numentries);
+  int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys);
+  int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age);
+  int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es);
+  int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es);
+  OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx);
+  OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s);
+  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);
+  int SSL_ech_set1_outer_alpn_protos(SSL *s, const unsigned char *protos,
+                                     const size_t protos_len);
+  int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni);
+  int SSL_ech_set1_grease_suite(SSL *s, const char *suite);
+  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,
+                                         const size_t protos_len);
+  int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len);
+
+=head1 DESCRIPTION
+
+TODO(ECH): Complete this text...
+
+The Encrypted Client Hello (ECH) APIs described here are built around
+the concept of an `OSSL_ECHSTORE` which contains ECH configuration
+information relevant for the current 'SSL_CTX' or 'SSL' connection.
+
+Mention SSL_set1_echstore() is a thing
+Mention OSSL_ECHSTORE_new() is a thing
+Mention OSSL_ECHSTORE_free() is a thing
+Mention OSSL_ECHSTORE_new_config() is a thing
+Mention OSSL_ECHSTORE_write_pem() is a thing
+Mention OSSL_ECHSTORE_read_echconfiglist() is a thing
+Mention OSSL_ECHSTORE_get1_info() is a thing
+Mention OSSL_ECHSTORE_downselect() is a thing
+Mention OSSL_ECHSTORE_set1_key_and_read_pem() is a thing
+Mention OSSL_ECHSTORE_read_pem() is a thing
+Mention OSSL_ECHSTORE_num_keys() is a thing
+Mention OSSL_ECHSTORE_num_entries() is a thing
+Mention OSSL_ECHSTORE_flush_keys() is a thing
+Mention SSL_CTX_set1_echstore() is a thing
+Mention SSL_CTX_get1_echstore() is a thing
+Mention SSL_get1_echstore() is a thing
+Mention SSL_ech_set1_server_names() is a thing
+Mention SSL_ech_set1_outer_server_name() is a thing
+Mention SSL_ech_set1_outer_alpn_protos() is a thing
+Mention SSL_ech_get1_status() is a thing
+Mention SSL_ech_set1_grease_suite() is a thing
+Mention SSL_ech_set_grease_type() is a thing
+Mention SSL_ech_set_callback() is a thing
+Mention SSL_ech_get1_retry_config() is a thing
+Mention SSL_CTX_ech1_set_outer_alpn_protos() is a thing
+Mention SSL_CTX_ech_raw_decrypt() is a thing
+Mention SSL_CTX_ech_set_callback() is a thing
+Mention SSL_set1_ech_config_list() is a thing
+
+=head2 Callback Function
+
+Applications can set a callback function that will be called when the
+outcome from an attempt at ECH has been determined. On the server,
+that happens early, as part of construction of the ServerHello message.
+On the client, the callback will happen after the SeverHello has
+been processed. In the event of HelloRetryRequest, the callback will
+only be triggered when processing the second ServerHello. The callback
+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);
+
+To set a callback function use SSL_ech_set_callback() or
+SSL_CTX_ech_set_callback() - the I input should match the
+above prototype.
+
+When the callback function is called, the I will point at a string
+intended for logging describing the state of ECH processing.
+Applications should not attempt to parse that string as the value depends
+on compile time settings, local configuration and the specific processing
+that happened prior to the callback. Applications that need to branch based
+on the outcome of ECH processing should instead make a call to
+SSL_ech_get1_status() from within their callback function.
+
+An example string I as seen on a client might be:
+
+ ech_attempted=1
+ ech_attempted_type=0xfe0d
+ ech_atttempted_cid=0x5d
+ ech_done=1
+ ech_grease=0
+ ech_returned_len=0
+ 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]
+
+=head1 RETURN VALUES
+
+All functions named here return one on success and zero on error.
+
+SSL_set1_echstore() returns zero on error
+SSL_set1_ech_config_list() returns zero on error
+OSSL_ECHSTORE_new() returns zero on error
+OSSL_ECHSTORE_free() returns zero on error
+OSSL_ECHSTORE_new_config() returns zero on error
+OSSL_ECHSTORE_write_pem() returns zero on error
+OSSL_ECHSTORE_read_echconfiglist() returns zero on error
+OSSL_ECHSTORE_get1_info() returns zero on error
+OSSL_ECHSTORE_downselect() returns zero on error
+OSSL_ECHSTORE_set1_key_and_read_pem() returns zero on error
+OSSL_ECHSTORE_read_pem() returns zero on error
+OSSL_ECHSTORE_num_keys() returns zero on error
+OSSL_ECHSTORE_num_entries() returns zero on error
+OSSL_ECHSTORE_flush_keys() returns zero on error
+SSL_CTX_set1_echstore() returns zero on error
+SSL_CTX_get1_echstore() returns zero on error
+SSL_get1_echstore() returns zero on error
+SSL_ech_set_server_names() returns zero on error
+SSL_ech_set_outer_server_name() returns zero on error
+SSL_ech_set_outer_alpn_protos() returns zero on error
+SSL_ech_get1_status() returns zero on error
+SSL_ech_set_grease_suite() returns zero on error
+SSL_ech_set_grease_type() returns zero on error
+SSL_ech_set_callback() returns zero on error
+SSL_ech_get_retry_config() returns zero on error
+SSL_CTX_ech_set1_outer_alpn_protos() returns zero on error
+SSL_CTX_ech_raw_decrypt() returns zero on error
+SSL_CTX_ech_set_callback() returns zero on error
+
+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()
+which (unusually for OpenSSL) return 0 on success and 1 on error.
+
+=head1 SEE ALSO
+
+The Encrypted ClientHello specification: L
+TODO(ECH) update link to RFC.
+
+=head1 HISTORY
+
+The functionality described here was added in OpenSSL 3.5.
+
+=head1 COPYRIGHT
+
+Copyright 2024 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
+L.
+
+=cut
diff --git a/include/internal/ech_helpers.h b/include/internal/ech_helpers.h
new file mode 100644
index 0000000000000..946ad2f1dcff4
--- /dev/null
+++ b/include/internal/ech_helpers.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ * Licensed under the OpenSSL license (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
+ */
+
+/*
+ * These functions are ECH helpers that are used within the library but
+ * also by ECH test code.
+ */
+
+#ifndef OPENSSL_ECH_HELPERS_H
+# define OPENSSL_ECH_HELPERS_H
+# pragma once
+
+# ifndef OPENSSL_NO_ECH
+
+/*
+ * the max HPKE 'info' we'll process is the max ECHConfig size
+ * (OSSL_ECH_MAX_ECHCONFIG_LEN) plus OSSL_ECH_CONTEXT_STRING(len=7) + 1
+ */
+#  define OSSL_ECH_MAX_INFO_LEN (OSSL_ECH_MAX_ECHCONFIG_LEN + 8)
+
+int ossl_ech_make_enc_info(const unsigned char *encoding,
+                           size_t encoding_length,
+                           unsigned char *info, size_t *info_len);
+
+/*
+ * Given a CH find the offsets of the session id, extensions and ECH
+ * ch is the encoded client hello
+ * ch_len is the length of ch
+ * sessid_off returns offset of session_id length
+ * exts_off points to offset of extensions
+ * exts_len returns length of extensions
+ * ech_off returns offset of ECH
+ * echtype returns the ext type of the ECH
+ * ech_len returns the length of the ECH
+ * sni_off returns offset of (outer) SNI
+ * sni_len returns the length of the SNI
+ * inner 1 if the ECH is marked as an inner, 0 for outer
+ * return 1 for success, other otherwise
+ *
+ * Offsets are set to zero if relevant thing not found.
+ * Offsets are returned to the type or length field in question.
+ *
+ * Note: input here is untrusted!
+ */
+int ossl_ech_helper_get_ch_offsets(const unsigned char *ch, size_t ch_len,
+                                   size_t *sessid_off, size_t *exts_off,
+                                   size_t *exts_len,
+                                   size_t *ech_off, uint16_t *echtype,
+                                   size_t *ech_len, size_t *sni_off,
+                                   size_t *sni_len, int *inner);
+
+# endif
+#endif
diff --git a/include/internal/packet.h b/include/internal/packet.h
index 2051cabd99d02..cce3bf5c9ff4e 100644
--- a/include/internal/packet.h
+++ b/include/internal/packet.h
@@ -22,6 +22,8 @@
 typedef struct {
     /* Pointer to where we are currently reading from */
     const unsigned char *curr;
+    /* Pointer to the start of the message */
+    const unsigned char *msgstart;
     /* Number of bytes remaining */
     size_t remaining;
 } PACKET;
@@ -52,6 +54,15 @@ static ossl_inline const unsigned char *PACKET_end(const PACKET *pkt)
     return pkt->curr + pkt->remaining;
 }
 
+/*
+ * Returns a pointer to the very start of the buffer. If this is a sub packet
+ * this will be the start of the buffer for the top of the PACKET tree.
+ */
+static ossl_inline const unsigned char *PACKET_msg_start(const PACKET *pkt)
+{
+    return pkt->msgstart;
+}
+
 /*
  * Returns a pointer to the PACKET's current position.
  * For use in non-PACKETized APIs.
@@ -74,7 +85,7 @@ __owur static ossl_inline int PACKET_buf_init(PACKET *pkt,
     if (len > (size_t)(SIZE_MAX / 2))
         return 0;
 
-    pkt->curr = buf;
+    pkt->curr = pkt->msgstart = buf;
     pkt->remaining = len;
     return 1;
 }
@@ -82,7 +93,7 @@ __owur static ossl_inline int PACKET_buf_init(PACKET *pkt,
 /* Initialize a PACKET to hold zero bytes. */
 static ossl_inline void PACKET_null_init(PACKET *pkt)
 {
-    pkt->curr = NULL;
+    pkt->curr = pkt->msgstart = NULL;
     pkt->remaining = 0;
 }
 
@@ -110,7 +121,11 @@ __owur static ossl_inline int PACKET_peek_sub_packet(const PACKET *pkt,
     if (PACKET_remaining(pkt) < len)
         return 0;
 
-    return PACKET_buf_init(subpkt, pkt->curr, len);
+    if (!PACKET_buf_init(subpkt, pkt->curr, len))
+        return 0;
+
+    subpkt->msgstart = pkt->msgstart;
+    return 1;
 }
 
 /*
@@ -543,6 +558,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_1(PACKET *pkt,
 
     *pkt = tmp;
     subpkt->curr = data;
+    subpkt->msgstart = pkt->msgstart;
     subpkt->remaining = length;
 
     return 1;
@@ -566,6 +582,7 @@ __owur static ossl_inline int PACKET_as_length_prefixed_1(PACKET *pkt,
 
     *pkt = tmp;
     subpkt->curr = data;
+    subpkt->msgstart = pkt->msgstart;
     subpkt->remaining = length;
 
     return 1;
@@ -592,6 +609,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_2(PACKET *pkt,
 
     *pkt = tmp;
     subpkt->curr = data;
+    subpkt->msgstart = pkt->msgstart;
     subpkt->remaining = length;
 
     return 1;
@@ -616,6 +634,7 @@ __owur static ossl_inline int PACKET_as_length_prefixed_2(PACKET *pkt,
 
     *pkt = tmp;
     subpkt->curr = data;
+    subpkt->msgstart = pkt->msgstart;
     subpkt->remaining = length;
 
     return 1;
@@ -641,6 +660,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_3(PACKET *pkt,
 
     *pkt = tmp;
     subpkt->curr = data;
+    subpkt->msgstart = pkt->msgstart;
     subpkt->remaining = length;
 
     return 1;
diff --git a/include/internal/ssl.h b/include/internal/ssl.h
index 689f3484ff7b8..5b55cd0e3a918 100644
--- a/include/internal/ssl.h
+++ b/include/internal/ssl.h
@@ -23,4 +23,9 @@ int ossl_ssl_get_error(const SSL *s, int i, int check_err);
 /* Set if this is our QUIC handshake layer */
 # define TLS1_FLAGS_QUIC_INTERNAL                0x4000
 
+/* We limit the number of key shares sent */
+# ifndef OPENSSL_CLIENT_MAX_KEY_SHARES
+#  define OPENSSL_CLIENT_MAX_KEY_SHARES 4
+# endif
+
 #endif
diff --git a/include/openssl/ech.h b/include/openssl/ech.h
new file mode 100644
index 0000000000000..74f6deed9eee8
--- /dev/null
+++ b/include/openssl/ech.h
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (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
+ */
+
+/*
+ * Externally-visible data structures and prototypes for handling
+ * Encrypted ClientHello (ECH).
+ */
+#ifndef OPENSSL_ECH_H
+# define OPENSSL_ECH_H
+# pragma once
+
+# include 
+# include 
+
+# ifndef OPENSSL_NO_ECH
+
+/*
+ * Some externally visible limits - most used for sanity checks that could be
+ * bigger if needed, but that work for now
+ */
+#  define OSSL_ECH_MAX_PAYLOAD_LEN 1500 /* max ECH ciphertext to en/decode */
+#  define OSSL_ECH_MIN_ECHCONFIG_LEN 32 /* min for all encodings */
+#  define OSSL_ECH_MAX_ECHCONFIG_LEN 1500 /* max for all encodings */
+#  define OSSL_ECH_MAX_ECHCONFIGEXT_LEN 512 /* ECHConfig extension max */
+#  define OSSL_ECH_MAX_MAXNAMELEN 255 /* ECHConfig max for max name length */
+#  define OSSL_ECH_MAX_PUBLICNAME 255 /* max ECHConfig public name length */
+#  define OSSL_ECH_MAX_ALPNLEN 255 /* max alpn length */
+#  define OSSL_ECH_OUTERS_MAX 20 /* max extensions we compress via outer-exts */
+#  define OSSL_ECH_ALLEXTS_MAX 32 /* max total number of extension we allow */
+
+/*
+ * ECH version. We only support RFC XXXX as of now.  As/if new ECHConfig
+ * versions are added, those will be noted here.
+ * TODO(ECH): Replace XXXX with the actual RFC number once known.
+ */
+#  define OSSL_ECH_RFCXXXX_VERSION 0xfe0d /* official ECHConfig version */
+/* latest version from an RFC */
+#  define OSSL_ECH_CURRENT_VERSION OSSL_ECH_RFCXXXX_VERSION
+
+/* Return codes from SSL_ech_get1_status */
+#  define SSL_ECH_STATUS_BACKEND    4 /* ECH backend: saw an ech_is_inner */
+#  define SSL_ECH_STATUS_GREASE_ECH 3 /* GREASEd and got an ECH in return */
+#  define SSL_ECH_STATUS_GREASE     2 /* ECH GREASE happened  */
+#  define SSL_ECH_STATUS_SUCCESS    1 /* Success */
+#  define SSL_ECH_STATUS_FAILED     0 /* Some internal or protocol error */
+#  define SSL_ECH_STATUS_BAD_CALL   -100 /* Some in/out arguments were NULL */
+#  define SSL_ECH_STATUS_NOT_TRIED  -101 /* ECH wasn't attempted  */
+#  define SSL_ECH_STATUS_BAD_NAME   -102 /* ECH ok but server cert bad */
+#  define SSL_ECH_STATUS_NOT_CONFIGURED -103 /* ECH wasn't configured */
+#  define SSL_ECH_STATUS_FAILED_ECH -105 /* Tried, failed, got an ECH, from a good name */
+#  define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* Tried, failed, got an ECH, from a bad name */
+
+/* if a caller wants to index the last entry in the store */
+#  define OSSL_ECHSTORE_LAST -1
+/* if a caller wants all entries in the store, e.g. to print public values */
+#  define OSSL_ECHSTORE_ALL -2
+
+/* Values for the for_retry inputs */
+#  define OSSL_ECH_FOR_RETRY 1
+#  define OSSL_ECH_NO_RETRY  0
+
+/*
+ * API calls built around OSSL_ECHSTORE
+ */
+OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq);
+void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es);
+int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
+                             uint16_t echversion, uint8_t max_name_length,
+                             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_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);
+int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry);
+int OSSL_ECHSTORE_num_entries(const OSSL_ECHSTORE *es, int *numentries);
+int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys);
+int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age);
+
+/*
+ * APIs relating OSSL_ECHSTORE to SSL/SSL_CTX
+ */
+int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es);
+int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es);
+
+OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx);
+OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s);
+
+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);
+/*
+ * Note that this function returns 1 for success and 0 for error. This
+ * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL)
+ * returns 0 for success and 1 on error.
+ */
+int SSL_ech_set1_outer_alpn_protos(SSL *s, const unsigned char *protos,
+                                   const size_t protos_len);
+
+int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni);
+int SSL_ech_set1_grease_suite(SSL *s, const char *suite);
+int SSL_ech_set_grease_type(SSL *s, uint16_t type);
+typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str);
+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);
+
+/*
+ * Note that this function returns 1 for success and 0 for error. This
+ * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL)
+ * returns 0 for success and 1 on error.
+ */
+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);
+
+# endif
+#endif
diff --git a/include/openssl/pem.h b/include/openssl/pem.h
index de1b6581f28f6..214c6cd5b9da5 100644
--- a/include/openssl/pem.h
+++ b/include/openssl/pem.h
@@ -60,6 +60,7 @@ extern "C" {
 # define PEM_STRING_SM2PRIVATEKEY "SM2 PRIVATE KEY"
 # define PEM_STRING_SM2PARAMETERS "SM2 PARAMETERS"
 # define PEM_STRING_ACERT        "ATTRIBUTE CERTIFICATE"
+# define PEM_STRING_ECHCONFIG    "ECHCONFIG"
 
 # define PEM_TYPE_ENCRYPTED      10
 # define PEM_TYPE_MIC_ONLY       20
diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in
index cb30dda92d2c8..f9f825a253570 100644
--- a/include/openssl/ssl.h.in
+++ b/include/openssl/ssl.h.in
@@ -44,6 +44,9 @@ use OpenSSL::stackhash qw(generate_stack_macros generate_const_stack_macros);
 # include 
 # include 
 # include 
+# ifndef OPENSSL_NO_ECH
+#  include 
+# endif
 # ifndef OPENSSL_NO_STDIO
 #  include 
 # endif
@@ -429,6 +432,28 @@ typedef int (*SSL_async_callback_fn)(SSL *s, void *arg);
 # define SSL_OP_PREFER_NO_DHE_KEX                        SSL_OP_BIT(35)
 # define SSL_OP_LEGACY_EC_POINT_FORMATS                  SSL_OP_BIT(36)
 
+
+#ifndef OPENSSL_NO_ECH
+/* 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)
+#endif
+
 /*
  * Option "collections."
  */
@@ -1194,6 +1219,9 @@ DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION)
 # define SSL_AD_NO_RENEGOTIATION         TLS1_AD_NO_RENEGOTIATION
 # define SSL_AD_MISSING_EXTENSION        TLS13_AD_MISSING_EXTENSION
 # define SSL_AD_CERTIFICATE_REQUIRED     TLS13_AD_CERTIFICATE_REQUIRED
+# ifndef OPENSSL_NO_ECH
+#  define SSL_AD_ECH_REQUIRED            TLS1_AD_ECH_REQUIRED
+# endif
 # define SSL_AD_UNSUPPORTED_EXTENSION    TLS1_AD_UNSUPPORTED_EXTENSION
 # define SSL_AD_CERTIFICATE_UNOBTAINABLE TLS1_AD_CERTIFICATE_UNOBTAINABLE
 # define SSL_AD_UNRECOGNIZED_NAME        TLS1_AD_UNRECOGNIZED_NAME
diff --git a/include/openssl/sslerr.h b/include/openssl/sslerr.h
index 53e0decbe6768..b1f10f5685087 100644
--- a/include/openssl/sslerr.h
+++ b/include/openssl/sslerr.h
@@ -36,6 +36,7 @@
 # define SSL_R_BAD_DIGEST_LENGTH                          111
 # define SSL_R_BAD_EARLY_DATA                             233
 # define SSL_R_BAD_ECC_CERT                               304
+# define SSL_R_BAD_ECHCONFIG_EXTENSION                    425
 # define SSL_R_BAD_ECPOINT                                306
 # define SSL_R_BAD_EXTENSION                              110
 # define SSL_R_BAD_HANDSHAKE_LENGTH                       332
@@ -113,6 +114,8 @@
 # define SSL_R_DUPLICATE_COMPRESSION_ID                   309
 # define SSL_R_ECC_CERT_NOT_FOR_SIGNING                   318
 # define SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE              374
+# define SSL_R_ECH_DECODE_ERROR                           426
+# define SSL_R_ECH_REQUIRED                               424
 # define SSL_R_EE_KEY_TOO_SMALL                           399
 # define SSL_R_EMPTY_RAW_PUBLIC_KEY                       349
 # define SSL_R_EMPTY_SRTP_PROTECTION_PROFILE_LIST         354
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 50910d0e4c0b5..0935828ddd0ae 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -78,6 +78,9 @@ extern "C" {
 # define TLS1_AD_BAD_CERTIFICATE_HASH_VALUE 114
 # define TLS1_AD_UNKNOWN_PSK_IDENTITY    115/* fatal */
 # define TLS1_AD_NO_APPLICATION_PROTOCOL 120 /* fatal */
+# ifndef OPENSSL_NO_ECH
+#  define TLS1_AD_ECH_REQUIRED           121 /* fatal */
+# endif
 
 /* ExtensionType values from RFC3546 / RFC4366 / RFC6066 */
 # define TLSEXT_TYPE_server_name                 0
@@ -168,6 +171,11 @@ extern "C" {
 #  define TLSEXT_TYPE_next_proto_neg              13172
 # endif
 
+# ifndef OPENSSL_NO_ECH
+#  define TLSEXT_TYPE_ech                       0xfe0d
+#  define TLSEXT_TYPE_outer_extensions          0xfd00
+# endif
+
 /* NameType value from RFC3546 */
 # define TLSEXT_NAMETYPE_host_name 0
 /* status request value from RFC3546 */
diff --git a/include/openssl/types.h b/include/openssl/types.h
index a83811e748510..8cb13af0b0a93 100644
--- a/include/openssl/types.h
+++ b/include/openssl/types.h
@@ -235,6 +235,12 @@ typedef struct ossl_decoder_ctx_st OSSL_DECODER_CTX;
 
 typedef struct ossl_self_test_st OSSL_SELF_TEST;
 
+#ifndef OPENSSL_NO_ECH
+/* opaque type for ECH related information */
+typedef struct ossl_echstore_st OSSL_ECHSTORE;
+#endif
+
+
 #ifdef  __cplusplus
 }
 #endif
diff --git a/ssl/build.info b/ssl/build.info
index 7f4ecaa68f50b..d5166e64220a3 100644
--- a/ssl/build.info
+++ b/ssl/build.info
@@ -2,6 +2,10 @@ SUBDIRS=record rio quic
 
 LIBS=../libssl
 
+IF[{- !$disabled{ech} -}]
+  SUBDIRS=ech
+ENDIF
+
 SOURCE[../libssl]=\
         pqueue.c \
         statem/statem_srvr.c statem/statem_clnt.c  s3_lib.c  s3_enc.c \
diff --git a/ssl/ech/build.info b/ssl/ech/build.info
new file mode 100644
index 0000000000000..7f60fb957c531
--- /dev/null
+++ b/ssl/ech/build.info
@@ -0,0 +1,3 @@
+$LIBSSL=../../libssl
+
+SOURCE[$LIBSSL]=ech_ssl_apis.c ech_store.c ech_internal.c ech_helper.c
diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c
new file mode 100644
index 0000000000000..b2c2a87aa4f8c
--- /dev/null
+++ b/ssl/ech/ech_helper.c
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include "../ssl_local.h"
+#include "ech_local.h"
+#include "internal/ech_helpers.h"
+
+/* used in ECH crypto derivations (odd format for EBCDIC goodness) */
+/* "tls ech" */
+static const char OSSL_ECH_CONTEXT_STRING[] = "\x74\x6c\x73\x20\x65\x63\x68";
+
+/*
+ * Construct HPKE "info" input as per spec
+ * encoding is the ECHconfig being used
+ * encoding_length is the length of ECHconfig being used
+ * info is a caller-allocated buffer for results
+ * info_len is the buffer size on input, used-length on output
+ * return 1 for success, zero otherwise
+ */
+int ossl_ech_make_enc_info(const unsigned char *encoding,
+                           size_t encoding_length,
+                           unsigned char *info, size_t *info_len)
+{
+    WPACKET ipkt = { 0 };
+
+    if (encoding == NULL || info == NULL || info_len == NULL)
+        return 0;
+    if (!WPACKET_init_static_len(&ipkt, info, *info_len, 0)
+        || !WPACKET_memcpy(&ipkt, OSSL_ECH_CONTEXT_STRING,
+                           sizeof(OSSL_ECH_CONTEXT_STRING) - 1)
+        /*
+         * the zero valued octet is required by the spec, section 7.1 so
+         * a tiny bit better to add it explicitly rather than depend on
+         * the context string being NUL terminated
+         */
+        || !WPACKET_put_bytes_u8(&ipkt, 0)
+        || !WPACKET_memcpy(&ipkt, encoding, encoding_length)
+        || !WPACKET_get_total_written(&ipkt, info_len)) {
+        WPACKET_cleanup(&ipkt);
+        return 0;
+    }
+    WPACKET_cleanup(&ipkt);
+    return 1;
+}
+
+/*
+ * Given a CH find the offsets of the session id, extensions and ECH
+ * ch is the encoded client hello
+ * ch_len is the length of ch
+ * sessid_off returns offset of session_id length
+ * exts_off points to offset of extensions
+ * exts_len returns length of extensions
+ * ech_off returns offset of ECH
+ * echtype returns the ext type of the ECH
+ * ech_len returns the length of the ECH
+ * sni_off returns offset of (outer) SNI
+ * sni_len returns the length of the SNI
+ * inner 1 if the ECH is marked as an inner, 0 for outer
+ * return 1 for success, other otherwise
+ *
+ * Offsets are set to zero if relevant thing not found.
+ * Offsets are returned to the type or length field in question.
+ *
+ * Note: input here is untrusted!
+ */
+int ossl_ech_helper_get_ch_offsets(const unsigned char *ch, size_t ch_len,
+                                   size_t *sessid_off, size_t *exts_off,
+                                   size_t *exts_len,
+                                   size_t *ech_off, uint16_t *echtype,
+                                   size_t *ech_len, size_t *sni_off,
+                                   size_t *sni_len, int *inner)
+{
+    unsigned int elen = 0, etype = 0, pi_tmp = 0;
+    const unsigned char *pp_tmp = NULL, *chstart = NULL, *estart = NULL;
+    PACKET pkt;
+    int done = 0;
+
+    if (ch == NULL || ch_len == 0 || sessid_off == NULL || exts_off == NULL
+        || ech_off == NULL || echtype == NULL || ech_len == NULL
+        || sni_off == NULL || inner == NULL)
+        return 0;
+    *sessid_off = *exts_off = *ech_off = *sni_off = *sni_len = *ech_len = 0;
+    *echtype = 0xffff;
+    if (!PACKET_buf_init(&pkt, ch, ch_len))
+        return 0;
+    chstart = PACKET_data(&pkt);
+    if (!PACKET_get_net_2(&pkt, &pi_tmp))
+        return 0;
+    /* if we're not TLSv1.2+ then we can bail, but it's not an error */
+    if (pi_tmp != TLS1_2_VERSION && pi_tmp != TLS1_3_VERSION)
+        return 1;
+    /* chew up the packet to extensions */
+    if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE)
+        || (*sessid_off = PACKET_data(&pkt) - chstart) == 0
+        || !PACKET_get_1(&pkt, &pi_tmp) /* sessid len */
+        || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* sessid */
+        || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite len */
+        || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* suites */
+        || !PACKET_get_1(&pkt, &pi_tmp) /* compression meths */
+        || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* comp meths */
+        || (*exts_off = PACKET_data(&pkt) - chstart) == 0
+        || !PACKET_get_net_2(&pkt, &pi_tmp) /* len(extensions) */
+        || (*exts_len = (size_t) pi_tmp) == 0)
+        /*
+         * unexpectedly, we return 1 here, as doing otherwise will
+         * break some non-ECH test code that truncates CH messages
+         * The same is true below when looking through extensions.
+         * That's ok though, we'll only set those offsets we've
+         * found.
+         */
+        return 1;
+    /* no extensions is theoretically ok, if uninteresting */
+    if (*exts_len == 0)
+        return 1;
+    /* find what we want from extensions */
+    estart = PACKET_data(&pkt);
+    while (PACKET_remaining(&pkt) > 0
+           && (size_t)(PACKET_data(&pkt) - estart) < *exts_len
+           && done < 2) {
+        if (!PACKET_get_net_2(&pkt, &etype)
+            || !PACKET_get_net_2(&pkt, &elen))
+            return 1; /* see note above */
+        if (etype == TLSEXT_TYPE_ech) {
+            if (elen == 0)
+                return 0;
+            *ech_off = PACKET_data(&pkt) - chstart - 4;
+            *echtype = etype;
+            *ech_len = elen;
+            done++;
+        }
+        if (etype == TLSEXT_TYPE_server_name) {
+            *sni_off = PACKET_data(&pkt) - chstart - 4;
+            *sni_len = elen;
+            done++;
+        }
+        if (!PACKET_get_bytes(&pkt, &pp_tmp, elen))
+            return 1; /* see note above */
+        if (etype == TLSEXT_TYPE_ech)
+            *inner = pp_tmp[0];
+    }
+    return 1;
+}
diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
new file mode 100644
index 0000000000000..2c96ded096a3b
--- /dev/null
+++ b/ssl/ech/ech_internal.c
@@ -0,0 +1,2207 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include 
+#include 
+#include "internal/ech_helpers.h"
+#include "internal/ssl_unwrap.h"
+#include "../ssl_local.h"
+#include "../statem/statem_local.h"
+#include "ech_local.h"
+
+#ifndef OPENSSL_NO_ECH
+
+/*
+ * Strings used in ECH crypto derivations (odd format for EBCDIC goodness)
+ */
+/* "ech accept confirmation" */
+static const char OSSL_ECH_ACCEPT_CONFIRM_STRING[] = "\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e";
+/* "hrr ech accept confirmation" */
+static const char OSSL_ECH_HRR_CONFIRM_STRING[] = "\x68\x72\x72\x20\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e";
+
+/* ECH internal API functions */
+
+# ifdef OSSL_ECH_SUPERVERBOSE
+/* ascii-hex print a buffer nicely for debug/interop purposes */
+void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen)
+{
+    OSSL_TRACE_BEGIN(TLS) {
+        if (msg == NULL) {
+            BIO_printf(trc_out, "msg is NULL\n");
+        } else if (buf == NULL || blen == 0) {
+            BIO_printf(trc_out, "%s: buf is %p\n", msg, (void *)buf);
+            BIO_printf(trc_out, "%s: blen is %lu\n", msg, (unsigned long)blen);
+        } else {
+            BIO_printf(trc_out, "%s (%lu)\n", msg, (unsigned long)blen);
+            BIO_dump_indent(trc_out, buf, (int)blen, 4);
+        }
+    } OSSL_TRACE_END(TLS);
+    return;
+}
+
+/* trace out transcript */
+static void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg)
+{
+    size_t hdatalen = 0;
+    unsigned char *hdata = NULL;
+    unsigned char ddata[EVP_MAX_MD_SIZE];
+    size_t ddatalen;
+
+    if (s == NULL)
+        return;
+    hdatalen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
+    ossl_ech_pbuf(msg, hdata, hdatalen);
+    if (s->s3.handshake_dgst != NULL) {
+        if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) {
+            OSSL_TRACE(TLS, "ssl_handshake_hash failed\n");
+            ossl_ech_pbuf(msg, ddata, ddatalen);
+        }
+    }
+    OSSL_TRACE(TLS, "new transbuf:\n");
+    ossl_ech_pbuf(msg, s->ext.ech.transbuf, s->ext.ech.transbuf_len);
+    return;
+}
+# endif
+
+static OSSL_ECHSTORE_ENTRY *ossl_echstore_entry_dup(const OSSL_ECHSTORE_ENTRY *orig)
+{
+    OSSL_ECHSTORE_ENTRY *ret = NULL;
+
+    if (orig == NULL)
+        return NULL;
+    ret = OPENSSL_zalloc(sizeof(*ret));
+    if (ret == NULL)
+        return NULL;
+    ret->version = orig->version;
+    if (orig->public_name != NULL) {
+        ret->public_name = OPENSSL_strdup(orig->public_name);
+        if (ret->public_name == NULL)
+            goto err;
+    }
+    ret->pub_len = orig->pub_len;
+    if (orig->pub != NULL) {
+        ret->pub = OPENSSL_memdup(orig->pub, orig->pub_len);
+        if (ret->pub == NULL)
+            goto err;
+    }
+    ret->nsuites = orig->nsuites;
+    ret->suites = OPENSSL_memdup(orig->suites, sizeof(OSSL_HPKE_SUITE) * ret->nsuites);
+    if (ret->suites == NULL)
+        goto err;
+    ret->max_name_length = orig->max_name_length;
+    ret->config_id = orig->config_id;
+    if (orig->exts != NULL) {
+        ret->exts = sk_OSSL_ECHEXT_deep_copy(orig->exts, ossl_echext_dup,
+                                             ossl_echext_free);
+        if (ret->exts == NULL)
+            goto err;
+    }
+    ret->loadtime = orig->loadtime;
+    if (orig->keyshare != NULL) {
+        if (!EVP_PKEY_up_ref(orig->keyshare))
+            goto err;
+        ret->keyshare = orig->keyshare;
+    }
+    ret->for_retry = orig->for_retry;
+    if (orig->encoded != NULL) {
+        ret->encoded_len = orig->encoded_len;
+        ret->encoded = OPENSSL_memdup(orig->encoded, ret->encoded_len);
+        if (ret->encoded == NULL)
+            goto err;
+    }
+    return ret;
+err:
+    ossl_echstore_entry_free(ret);
+    return NULL;
+}
+
+/* duplicate an OSSL_ECHSTORE as needed */
+OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old)
+{
+    OSSL_ECHSTORE *cp = NULL;
+
+    if (old == NULL)
+        return NULL;
+    cp = OPENSSL_zalloc(sizeof(*cp));
+    if (cp == NULL)
+        return NULL;
+    cp->libctx = old->libctx;
+    if (old->propq != NULL) {
+        cp->propq = OPENSSL_strdup(old->propq);
+        if (cp->propq == NULL)
+            goto err;
+    }
+    if (old->entries != NULL) {
+        cp->entries = sk_OSSL_ECHSTORE_ENTRY_deep_copy(old->entries,
+                                                       ossl_echstore_entry_dup,
+                                                       ossl_echstore_entry_free);
+        if (cp->entries == NULL)
+            goto err;
+    }
+    return cp;
+err:
+    OSSL_ECHSTORE_free(cp);
+    return NULL;
+}
+
+void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce)
+{
+    if (ce == NULL)
+        return;
+    OSSL_ECHSTORE_free(ce->es);
+    OPENSSL_free(ce->alpn_outer);
+    return;
+}
+
+static void ech_free_stashed_key_shares(OSSL_ECH_CONN *ec)
+{
+    size_t i;
+
+    if (ec == NULL)
+        return;
+    for (i = 0; i != ec->num_ks_pkey; i++) {
+        EVP_PKEY_free(ec->ks_pkey[i]);
+        ec->ks_pkey[i] = NULL;
+    }
+    ec->num_ks_pkey = 0;
+    return;
+}
+
+void ossl_ech_conn_clear(OSSL_ECH_CONN *ec)
+{
+    if (ec == NULL)
+        return;
+    OSSL_ECHSTORE_free(ec->es);
+    OPENSSL_free(ec->outer_hostname);
+    OPENSSL_free(ec->alpn_outer);
+    OPENSSL_free(ec->former_inner);
+    OPENSSL_free(ec->transbuf);
+    OPENSSL_free(ec->innerch);
+    OPENSSL_free(ec->grease_suite);
+    OPENSSL_free(ec->sent);
+    OPENSSL_free(ec->returned);
+    OPENSSL_free(ec->pub);
+    OSSL_HPKE_CTX_free(ec->hpke_ctx);
+    OPENSSL_free(ec->encoded_inner);
+    ech_free_stashed_key_shares(ec);
+    return;
+}
+
+/* called from ssl/ssl_lib.c: ossl_ssl_connection_new_int */
+int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx,
+                       const SSL_METHOD *method)
+{
+    memset(&s->ext.ech, 0, sizeof(s->ext.ech));
+    if (ctx->ext.ech.es != NULL
+        && (s->ext.ech.es = ossl_echstore_dup(ctx->ext.ech.es)) == NULL)
+        goto err;
+    s->ext.ech.cb = ctx->ext.ech.cb;
+    if (ctx->ext.ech.alpn_outer != NULL) {
+        s->ext.ech.alpn_outer = OPENSSL_memdup(ctx->ext.ech.alpn_outer,
+                                               ctx->ext.ech.alpn_outer_len);
+        if (s->ext.ech.alpn_outer == NULL)
+            goto err;
+        s->ext.ech.alpn_outer_len = ctx->ext.ech.alpn_outer_len;
+    }
+    /* initialise type/cid to unknown */
+    s->ext.ech.attempted_type = OSSL_ECH_type_unknown;
+    s->ext.ech.attempted_cid = OSSL_ECH_config_id_unset;
+    if (s->ext.ech.es != NULL)
+        s->ext.ech.attempted = 1;
+    if ((ctx->options & SSL_OP_ECH_GREASE) != 0)
+        s->options |= SSL_OP_ECH_GREASE;
+    return 1;
+err:
+    OSSL_ECHSTORE_free(s->ext.ech.es);
+    s->ext.ech.es = NULL;
+    OPENSSL_free(s->ext.ech.alpn_outer);
+    s->ext.ech.alpn_outer = NULL;
+    s->ext.ech.alpn_outer_len = 0;
+    return 0;
+}
+
+/*
+ * Assemble the set of ECHConfig values to return as retry-configs.
+ * The caller (stoc ECH extension handler) needs to OPENSSL_free the rcfgs
+ * The rcfgs itself is missing the outer length to make it an ECHConfigList
+ * so the caller adds that using WPACKET functions
+ */
+int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs,
+                               size_t *rcfgslen)
+{
+    OSSL_ECHSTORE *es = NULL;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    int i, num = 0;
+    size_t retslen = 0;
+    unsigned char *tmp = NULL, *rets = NULL;
+
+    if (s == NULL || rcfgs == NULL || rcfgslen == NULL)
+        return 0;
+    es = s->ext.ech.es;
+    if (es != NULL && es->entries != NULL)
+        num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
+    for (i = 0; i != num; i++) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
+        if (ee != NULL && ee->for_retry == OSSL_ECH_FOR_RETRY) {
+            if (ee->encoded_len > SIZE_MAX - retslen)
+                return 0;
+            tmp = (unsigned char *)OPENSSL_realloc(rets,
+                                                   retslen + ee->encoded_len);
+            if (tmp == NULL)
+                goto err;
+            rets = tmp;
+            memcpy(rets + retslen, ee->encoded, ee->encoded_len);
+            retslen += ee->encoded_len;
+        }
+    }
+    *rcfgs = rets;
+    *rcfgslen = retslen;
+    return 1;
+err:
+    OPENSSL_free(rets);
+    *rcfgs = NULL;
+    *rcfgslen = 0;
+    return 0;
+}
+
+/* GREASEy constants */
+# define OSSL_ECH_MAX_GREASE_PUB 0x100 /* buffer size for 'enc' values */
+# define OSSL_ECH_MAX_GREASE_CT 0x200 /* max GREASEy ciphertext we'll emit */
+
+/*
+ * 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:
+ *   - always HKDF-SHA256
+ *   - always AES-128-GCM
+ *   - random config ID, even for requests to same server in same session
+ *   - random enc
+ *   - random looking payload, randomly 144, 176, 208, 240 bytes, no correlation with server
+ */
+int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt)
+{
+    OSSL_HPKE_SUITE hpke_suite_in = OSSL_HPKE_SUITE_DEFAULT;
+    OSSL_HPKE_SUITE *hpke_suite_in_p = NULL;
+    OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+    size_t pp_at_start = 0, pp_at_end = 0;
+    size_t senderpub_len = OSSL_ECH_MAX_GREASE_PUB;
+    size_t cipher_len = 0, cipher_len_jitter = 0;
+    unsigned char cid, senderpub[OSSL_ECH_MAX_GREASE_PUB];
+    unsigned char cipher[OSSL_ECH_MAX_GREASE_CT];
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+
+    WPACKET_get_total_written(pkt, &pp_at_start);
+    /* randomly select cipher_len to be one of 144, 176, 208, 244 */
+    if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    cipher_len_jitter = cid % 4;
+    cipher_len = 144;
+    cipher_len += 32 * cipher_len_jitter;
+    /* generate a random (1 octet) client id */
+    if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    s->ext.ech.attempted_cid = cid;
+    hpke_suite_in_p = &hpke_suite;
+    if (s->ext.ech.grease_suite != NULL) {
+        if (OSSL_HPKE_str2suite(s->ext.ech.grease_suite, &hpke_suite_in) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        hpke_suite_in_p = &hpke_suite_in;
+    }
+    if (OSSL_HPKE_get_grease_value(hpke_suite_in_p, &hpke_suite,
+                                   senderpub, &senderpub_len,
+                                   cipher, cipher_len,
+                                   sctx->libctx, sctx->propq) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type)
+        || !WPACKET_start_sub_packet_u16(pkt)
+        || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE)
+        || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id)
+        || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id)
+        || !WPACKET_put_bytes_u8(pkt, cid)
+        || !WPACKET_sub_memcpy_u16(pkt, senderpub, senderpub_len)
+        || !WPACKET_sub_memcpy_u16(pkt, cipher, cipher_len)
+        || !WPACKET_close(pkt)
+        ) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    /* record the ECH sent so we can re-tx same if we hit an HRR */
+    OPENSSL_free(s->ext.ech.sent);
+    WPACKET_get_total_written(pkt, &pp_at_end);
+    s->ext.ech.sent_len = pp_at_end - pp_at_start;
+    s->ext.ech.sent = OPENSSL_malloc(s->ext.ech.sent_len);
+    if (s->ext.ech.sent == NULL) {
+        s->ext.ech.sent_len = 0;
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    memcpy(s->ext.ech.sent, WPACKET_get_curr(pkt) - s->ext.ech.sent_len,
+           s->ext.ech.sent_len);
+    s->ext.ech.grease = OSSL_ECH_IS_GREASE;
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "ECH - sending GREASE\n");
+    } OSSL_TRACE_END(TLS);
+    return 1;
+}
+
+/*
+ * Search the ECH store for one that's a match. If no outer_name was set via
+ * API then we just take the 1st match where we locally support the HPKE suite.
+ * If OTOH, an outer_name was provided via API then we prefer the first that
+ * matches that. Name comparison is via case-insensitive exact matches.
+ */
+int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee,
+                               OSSL_HPKE_SUITE *suite)
+{
+    int namematch = 0, nameoverride = 0, suitematch = 0, num, cind = 0;
+    unsigned int csuite = 0, tsuite = 0;
+    size_t hnlen = 0;
+    OSSL_ECHSTORE_ENTRY *lee = NULL, *tee = NULL;
+    OSSL_ECHSTORE *es = NULL;
+    char *hn = NULL;
+
+    if (s == NULL || s->ext.ech.es == NULL || ee == NULL || suite == NULL)
+        return 0;
+    *ee = NULL;
+    es = s->ext.ech.es;
+    if (es->entries == NULL)
+        return 0;
+    num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
+    /* allow API-set pref to override */
+    hn = s->ext.ech.outer_hostname;
+    hnlen = (hn == NULL ? 0 : (unsigned int)strlen(hn));
+    if (hnlen != 0)
+        nameoverride = 1;
+    if (s->ext.ech.no_outer == 1) {
+        hn = NULL;
+        hnlen = 0;
+        nameoverride = 1;
+    }
+    for (cind = 0; cind < num && (suitematch == 0 || namematch == 0); cind++) {
+        lee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cind);
+        if (lee == NULL || lee->version != OSSL_ECH_RFCXXXX_VERSION)
+            continue;
+        if (nameoverride == 1 && hnlen == 0) {
+            namematch = 1;
+        } else {
+            namematch = 0;
+            if (hnlen == 0
+                || (lee->public_name != NULL
+                    && strlen(lee->public_name) == hnlen
+                    && OPENSSL_strncasecmp(hn, (char *)lee->public_name,
+                                           hnlen) == 0))
+                namematch = 1;
+        }
+        suitematch = 0;
+        for (csuite = 0; csuite != lee->nsuites && suitematch == 0; csuite++) {
+            if (OSSL_HPKE_suite_check(lee->suites[csuite]) == 1) {
+                if (tee == NULL) { /* remember 1st suite match for override */
+                    tee = lee;
+                    tsuite = csuite;
+                }
+                suitematch = 1;
+                if (namematch == 1) { /* pick this one if both "fit" */
+                    *suite = lee->suites[csuite];
+                    *ee = lee;
+                    break;
+                }
+            }
+        }
+    }
+    if (tee != NULL && nameoverride == 1
+        && (namematch == 0 || suitematch == 0)) {
+        *suite = tee->suites[tsuite];
+        *ee = tee;
+    } else if (namematch == 0 || suitematch == 0) {
+        /* no joy */
+        return 0;
+    }
+    if (*ee == NULL || (*ee)->pub_len == 0 || (*ee)->pub == NULL)
+        return 0;
+    return 1;
+}
+
+/* Make up the ClientHelloInner and EncodedClientHelloInner buffers */
+int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded,
+                          size_t *encoded_len)
+{
+    int rv = 0;
+    size_t nraws = 0, ind = 0, innerlen = 0;
+    WPACKET inner = { 0 }; /* "fake" pkt for inner */
+    BUF_MEM *inner_mem = NULL;
+    RAW_EXTENSION *raws = NULL;
+
+    /* basic checks */
+    if (s == NULL)
+        return 0;
+    if (s->ext.ech.es == NULL || s->clienthello == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if ((inner_mem = BUF_MEM_new()) == NULL
+        || !WPACKET_init(&inner, inner_mem)
+        /* We don't add the type and 3-octet header as usually done */
+        /* Add ver/rnd/sess-id/suites to buffer */
+        || !WPACKET_put_bytes_u16(&inner, s->client_version)
+        || !WPACKET_memcpy(&inner, s->ext.ech.client_random, SSL3_RANDOM_SIZE)
+        /* Session ID is forced to zero in the encoded inner */
+        || !WPACKET_sub_memcpy_u8(&inner, NULL, 0)
+        /* Ciphers supported */
+        || !WPACKET_start_sub_packet_u16(&inner)
+        || !ssl_cipher_list_to_bytes(s, SSL_get_ciphers(&s->ssl), &inner)
+        || !WPACKET_close(&inner)
+        /* COMPRESSION */
+        || !WPACKET_start_sub_packet_u8(&inner)
+        /* Add the NULL compression method */
+        || !WPACKET_put_bytes_u8(&inner, 0)
+        || !WPACKET_close(&inner)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* Now handle extensions */
+    if (!WPACKET_start_sub_packet_u16(&inner)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* Grab a pointer to the already constructed extensions */
+    raws = s->clienthello->pre_proc_exts;
+    nraws = s->clienthello->pre_proc_exts_len;
+    if (raws == NULL || nraws < TLSEXT_IDX_num_builtins) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /*  We put ECH-compressed stuff first (if any), because we can */
+    if (s->ext.ech.n_outer_only > 0) {
+        if (!WPACKET_put_bytes_u16(&inner, TLSEXT_TYPE_outer_extensions)
+            || !WPACKET_start_sub_packet_u16(&inner)
+            /* redundant encoding of more-or-less the same thing */
+            || !WPACKET_start_sub_packet_u8(&inner)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        /* add the types for each of the compressed extensions now */
+        for (ind = 0; ind != s->ext.ech.n_outer_only; ind++) {
+            if (!WPACKET_put_bytes_u16(&inner, s->ext.ech.outer_only[ind])) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        }
+        /* close the 2 sub-packets with the compressed types */
+        if (!WPACKET_close(&inner) || !WPACKET_close(&inner)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    }
+    /* now copy the rest, as "proper" exts, into encoded inner */
+    for (ind = 0; ind < TLSEXT_IDX_num_builtins; ind++) {
+        if (raws[ind].present == 0 || ossl_ech_2bcompressed((int)ind) == 1)
+            continue;
+        if (!WPACKET_put_bytes_u16(&inner, raws[ind].type)
+            || !WPACKET_sub_memcpy_u16(&inner, PACKET_data(&raws[ind].data),
+                                       PACKET_remaining(&raws[ind].data))) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    }
+    if (!WPACKET_close(&inner)  /* close the encoded inner packet */
+        || !WPACKET_get_length(&inner, &innerlen)) { /* len for inner CH */
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    *encoded = (unsigned char *)inner_mem->data;
+    inner_mem->data = NULL; /* keep BUF_MEM_free happy */
+    *encoded_len = innerlen;
+    /* and clean up */
+    rv = 1;
+err:
+    WPACKET_cleanup(&inner);
+    BUF_MEM_free(inner_mem);
+    return rv;
+}
+
+/*
+ * Find ECH acceptance signal in a SH
+ * hrr is 1 if this is for an HRR, otherwise for SH
+ * acbuf is (a preallocated) 8 octet buffer
+ * shbuf is a pointer to the SH buffer
+ * shlen is the length of the SH buf
+ * return: 1 for success, 0 otherwise
+ */
+int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr,
+                          unsigned char acbuf[OSSL_ECH_SIGNAL_LEN])
+{
+    unsigned char *acp = NULL;
+
+    if (hrr == 0) {
+        acp = s->s3.server_random + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN;
+    } else { /* was set in extension handler */
+        if (s->ext.ech.hrrsignal_p == NULL)
+            return 0;
+        acp = s->ext.ech.hrrsignal;
+    }
+    memcpy(acbuf, acp, OSSL_ECH_SIGNAL_LEN);
+    return 1;
+}
+
+/*
+ * reset the handshake buffer for transcript after ECH is good
+ * buf is the data to put into the transcript (inner CH if no HRR)
+ * blen is the length of buf
+ * return 1 for success
+ */
+int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf,
+                             size_t blen)
+{
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("RESET transcript to", buf, blen);
+# endif
+    if (s->s3.handshake_buffer != NULL) {
+        if (BIO_reset(s->s3.handshake_buffer) < 0)
+            return 0;
+    } else {
+        s->s3.handshake_buffer = BIO_new(BIO_s_mem());
+        if (s->s3.handshake_buffer == NULL)
+            return 0;
+        (void)BIO_set_close(s->s3.handshake_buffer, BIO_CLOSE);
+    }
+    EVP_MD_CTX_free(s->s3.handshake_dgst);
+    s->s3.handshake_dgst = NULL;
+    /* providing nothing at all is a real use (mid-HRR) */
+    if (buf != NULL && blen > 0)
+        BIO_write(s->s3.handshake_buffer, (void *)buf, (int)blen);
+    return 1;
+}
+
+/*
+ * To control the number of zeros added after an EncodedClientHello - we pad
+ * to a target number of octets or, if there are naturally more, to a number
+ * divisible by the defined increment (we also do the spec-recommended SNI
+ * padding thing first)
+ */
+# define OSSL_ECH_PADDING_TARGET 128 /* ECH cleartext padded to at least this */
+# define OSSL_ECH_PADDING_INCREMENT 32 /* ECH padded to a multiple of this */
+
+/*
+ * figure out how much padding for cleartext (on client)
+ * ee is the chosen ECHConfig
+ * return overall length to use including padding or zero on error
+ *
+ * "Recommended" inner SNI padding scheme as per spec (section 6.1.3)
+ * Might remove the mnl stuff later - overall message padding seems
+ * better really, BUT... we might want to keep this if others (e.g.
+ * browsers) do it so as to not stand out compared to them.
+ *
+ * The "+ 9" constant below is from the specification and is the
+ * expansion comparing a string length to an encoded SNI extension.
+ * Same is true of the 31/32 formula below.
+ *
+ * Note that the AEAD tag will be added later, so if we e.g. have
+ * a padded cleartext of 128 octets, the ciphertext will be 144
+ * octets.
+ */
+size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
+                             size_t encoded_len)
+{
+    size_t length_of_padding = 0, length_with_snipadding = 0;
+    size_t innersnipadding = 0, length_with_padding = 0;
+    size_t mnl = 0, isnilen = 0;
+
+    if (s == NULL || ee == NULL)
+        return 0;
+    mnl = ee->max_name_length;
+    if (mnl != 0) {
+        /* do weirder padding if SNI present in inner */
+        if (s->ext.hostname != NULL) {
+            isnilen = strlen(s->ext.hostname) + 9;
+            innersnipadding = (mnl > isnilen) ? (int)(mnl - isnilen) : 0;
+        } else {
+            innersnipadding = (int)mnl + 9;
+        }
+    }
+    /* padding is after the inner client hello has been encoded */
+    length_with_snipadding = innersnipadding + (int)encoded_len;
+    length_of_padding = 31 - ((length_with_snipadding - 1) % 32);
+    length_with_padding = (int)encoded_len + length_of_padding
+        + innersnipadding;
+    /*
+     * 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.
+     */
+    if ((length_with_padding % OSSL_ECH_PADDING_INCREMENT) != 0)
+        length_with_padding += OSSL_ECH_PADDING_INCREMENT
+            - (length_with_padding % OSSL_ECH_PADDING_INCREMENT);
+    while (length_with_padding < OSSL_ECH_PADDING_TARGET)
+        length_with_padding += OSSL_ECH_PADDING_INCREMENT;
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %zu "
+                   "lop: %zu, clear_len (len with padding): %zu, orig: %zu\n",
+                   mnl, length_with_snipadding, length_of_padding,
+                   length_with_padding, encoded_len);
+    } OSSL_TRACE_END(TLS);
+    return length_with_padding;
+}
+
+/*
+ * Calculate AAD and do ECH encryption
+ * pkt is the packet to send
+ * return 1 for success, other otherwise
+ *
+ * 1. Make up the AAD: the encoded outer, with ECH ciphertext octets zero'd
+ * 2. Do the encryption
+ * 3. Put the ECH back into the encoding
+ * 4. Encode the outer (again!)
+ */
+int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt)
+{
+    int rv = 0;
+    size_t cipherlen = 0, aad_len = 0, mypub_len = 0, clear_len = 0;
+    size_t encoded_inner_len = 0;
+    unsigned char *clear = NULL, *aad = NULL, *mypub = NULL;
+    unsigned char *encoded_inner = NULL, *cipher_loc = NULL;
+
+    if (s == NULL)
+        return 0;
+    if (s->ext.ech.es == NULL || s->ext.ech.es->entries == NULL
+        || pkt == NULL || s->ssl.ctx == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* values calculated in tls_construct_ctos_ech */
+    encoded_inner = s->ext.ech.encoded_inner;
+    encoded_inner_len = s->ext.ech.encoded_inner_len;
+    clear_len = s->ext.ech.clearlen;
+    cipherlen = s->ext.ech.cipherlen;
+    if (!WPACKET_get_total_written(pkt, &aad_len) || aad_len < 4) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    aad_len -= 4; /* ECH/HPKE aad starts after type + 3-octet len */
+    aad = WPACKET_get_curr(pkt) - aad_len;
+    /* where we'll replace zeros with ciphertext */
+    cipher_loc = aad + s->ext.ech.cipher_offset;
+    /*
+     * close the extensions of the CH - we skipped doing this
+     * earlier when encoding extensions, to allow for adding the
+     * ECH here (when doing ECH) - see tls_construct_extensions()
+     * towards the end
+     */
+    if (!WPACKET_close(pkt)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EAAE: aad", aad, aad_len);
+# endif
+    clear = OPENSSL_zalloc(clear_len); /* zeros incl. padding */
+    if (clear == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    memcpy(clear, encoded_inner, encoded_inner_len);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EAAE: padded clear", clear, clear_len);
+# endif
+    /* we're done with this now */
+    OPENSSL_free(s->ext.ech.encoded_inner);
+    s->ext.ech.encoded_inner = NULL;
+    rv = OSSL_HPKE_seal(s->ext.ech.hpke_ctx, cipher_loc,
+                        &cipherlen, aad, aad_len, clear, clear_len);
+    OPENSSL_free(clear);
+    if (rv != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EAAE: cipher", cipher_loc, cipherlen);
+    ossl_ech_pbuf("EAAE: hpke mypub", mypub, mypub_len);
+    /* re-use aad_len for tracing */
+    WPACKET_get_total_written(pkt, &aad_len);
+    ossl_ech_pbuf("EAAE pkt aftr", WPACKET_get_curr(pkt) - aad_len, aad_len);
+# endif
+    return 1;
+err:
+    return 0;
+}
+
+/*
+ * print info about the ECH-status of an SSL connection
+ * out is the BIO to use (e.g. stdout/whatever)
+ * selector OSSL_ECH_SELECT_ALL or just one of the SSL_ECH values
+ */
+void ossl_ech_status_print(BIO *out, SSL_CONNECTION *s, int selector)
+{
+    int num = 0, i, has_priv, for_retry;
+    size_t j;
+    time_t secs = 0;
+    char *pn = NULL, *ec = NULL;
+    OSSL_ECHSTORE *es = NULL;
+
+# ifdef OSSL_ECH_SUPERVERBOSE
+    BIO_printf(out, "ech_status_print\n");
+    BIO_printf(out, "s=%p\n", (void *)s);
+# endif
+    BIO_printf(out, "ech_attempted=%d\n", s->ext.ech.attempted);
+    BIO_printf(out, "ech_attempted_type=0x%4x\n",
+               s->ext.ech.attempted_type);
+    if (s->ext.ech.attempted_cid == OSSL_ECH_config_id_unset)
+        BIO_printf(out, "ech_atttempted_cid is unset\n");
+    else
+        BIO_printf(out, "ech_atttempted_cid=0x%02x\n",
+                   s->ext.ech.attempted_cid);
+    BIO_printf(out, "ech_done=%d\n", s->ext.ech.done);
+    BIO_printf(out, "ech_grease=%d\n", s->ext.ech.grease);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    BIO_printf(out, "HRR=%d\n", s->hello_retry_request);
+# endif
+    BIO_printf(out, "ech_backend=%d\n", s->ext.ech.backend);
+    BIO_printf(out, "ech_success=%d\n", s->ext.ech.success);
+    es = s->ext.ech.es;
+    if (es == NULL || es->entries == NULL) {
+        BIO_printf(out, "ECH cfg=NONE\n");
+    } else {
+        num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
+        BIO_printf(out, "%d ECHConfig values loaded\n", num);
+        for (i = 0; i != num; i++) {
+            if (selector != OSSL_ECHSTORE_ALL && selector != i)
+                continue;
+            BIO_printf(out, "cfg(%d): ", i);
+            if (OSSL_ECHSTORE_get1_info(es, i, &secs, &pn, &ec,
+                                        &has_priv, &for_retry) != 1) {
+                OPENSSL_free(pn); /* just in case */
+                OPENSSL_free(ec);
+                continue;
+            }
+            BIO_printf(out, "ECH entry: %d public_name: %s age: %d%s\n",
+                       i, pn, (int)secs, has_priv ? " (has private key)" : "");
+            BIO_printf(out, "\t%s\n", ec);
+            OPENSSL_free(pn);
+            OPENSSL_free(ec);
+        }
+    }
+    if (s->ext.ech.returned) {
+        BIO_printf(out, "ret=");
+        for (j = 0; j != s->ext.ech.returned_len; j++) {
+            if (j != 0 && j % 16 == 0)
+                BIO_printf(out, "\n    ");
+            BIO_printf(out, "%02x:", (unsigned)(s->ext.ech.returned[j]));
+        }
+        BIO_printf(out, "\n");
+    }
+    return;
+}
+
+/*
+ * Swap the inner and outer after ECH success on the client
+ * return 0 for error, 1 for success
+ */
+int ossl_ech_swaperoo(SSL_CONNECTION *s)
+{
+    unsigned char *curr_buf = NULL;
+    size_t curr_buflen = 0;
+
+    if (s == NULL)
+        return 0;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_ptranscript(s, "ech_swaperoo, b4");
+# endif
+    /* un-stash inner key share(s) */
+    if (ossl_ech_unstash_keyshares(s) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    /*
+     * When not doing HRR... fix up the transcript to reflect the inner CH.
+     * If there's a client hello at the start of the buffer, then that's
+     * the outer CH and we want to replace that with the inner. We need to
+     * be careful that there could be early data or a server hello following
+     * and we can't lose that.
+     *
+     * For HRR... HRR processing code has already done the necessary.
+     */
+    if (s->hello_retry_request == SSL_HRR_NONE) {
+        BIO *handbuf = s->s3.handshake_buffer;
+        PACKET pkt, subpkt;
+        unsigned int mt;
+
+        s->s3.handshake_buffer = NULL;
+        if (ssl3_init_finished_mac(s) == 0) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            BIO_free(handbuf);
+            return 0;
+        }
+        if (ssl3_finish_mac(s, s->ext.ech.innerch, s->ext.ech.innerch_len) == 0) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            BIO_free(handbuf);
+            return 0;
+        }
+        curr_buflen = BIO_get_mem_data(handbuf, &curr_buf);
+        if (PACKET_buf_init(&pkt, curr_buf, curr_buflen)
+            && PACKET_get_1(&pkt, &mt)
+            && mt == SSL3_MT_CLIENT_HELLO
+            && PACKET_remaining(&pkt) >= 3) {
+            if (!PACKET_get_length_prefixed_3(&pkt, &subpkt)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                BIO_free(handbuf);
+                return 0;
+            }
+            if (PACKET_remaining(&pkt) > 0) {
+                if (ssl3_finish_mac(s, PACKET_data(&pkt), PACKET_remaining(&pkt)) == 0) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    BIO_free(handbuf);
+                    return 0;
+                }
+            }
+            BIO_free(handbuf);
+        }
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_ptranscript(s, "ech_swaperoo, after");
+# endif
+    /* Declare victory! */
+    s->ext.ech.attempted = 1;
+    s->ext.ech.success = 1;
+    s->ext.ech.done = 1;
+    s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
+    /* time to call an ECH callback, if there's one */
+    if (s->ext.ech.es != NULL && s->ext.ech.done == 1
+        && s->hello_retry_request != SSL_HRR_PENDING
+        && s->ext.ech.cb != NULL) {
+        char pstr[OSSL_ECH_PBUF_SIZE + 1] = { 0 };
+        BIO *biom = BIO_new(BIO_s_mem());
+        unsigned int cbrv = 0;
+
+        if (biom == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        ossl_ech_status_print(biom, s, OSSL_ECHSTORE_ALL);
+        BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE);
+        cbrv = s->ext.ech.cb(&s->ssl, pstr);
+        BIO_free(biom);
+        if (cbrv != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+    }
+    return 1;
+}
+
+/*
+ * do the HKDF for ECH acceptance checking
+ * md is the h/s hash
+ * for_hrr is 1 if we're doing a HRR
+ * hashval/hashlen is the transcript hash
+ * hoval is the output, with the ECH acceptance signal
+ * return 1 for good, 0 for error
+ */
+static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr,
+                                 unsigned char *hashval, size_t hashlen,
+                                 unsigned char hoval[OSSL_ECH_SIGNAL_LEN])
+{
+    int rv = 0;
+    unsigned char notsecret[EVP_MAX_MD_SIZE], zeros[EVP_MAX_MD_SIZE];
+    size_t retlen = 0, labellen = 0;
+    EVP_PKEY_CTX *pctx = NULL;
+    const char *label = NULL;
+    unsigned char *p = NULL;
+
+    if (for_hrr == 1) {
+        label = OSSL_ECH_HRR_CONFIRM_STRING;
+        labellen = sizeof(OSSL_ECH_HRR_CONFIRM_STRING) - 1;
+    } else {
+        label = OSSL_ECH_ACCEPT_CONFIRM_STRING;
+        labellen = sizeof(OSSL_ECH_ACCEPT_CONFIRM_STRING) - 1;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cc: label", (unsigned char *)label, labellen);
+# endif
+    memset(zeros, 0, EVP_MAX_MD_SIZE);
+    /* We don't seem to have an hkdf-extract that's exposed by libcrypto */
+    pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
+    if (pctx == NULL
+        || EVP_PKEY_derive_init(pctx) != 1
+        || EVP_PKEY_CTX_hkdf_mode(pctx,
+                                  EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1
+        || EVP_PKEY_CTX_set_hkdf_md(pctx, md) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* pick correct client_random */
+    if (s->server)
+        p = s->s3.client_random;
+    else
+        p = s->ext.ech.client_random;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cc: client_random", p, SSL3_RANDOM_SIZE);
+# endif
+    if (EVP_PKEY_CTX_set1_hkdf_key(pctx, p, SSL3_RANDOM_SIZE) != 1
+        || EVP_PKEY_CTX_set1_hkdf_salt(pctx, zeros, (int)hashlen) != 1
+        || EVP_PKEY_derive(pctx, NULL, &retlen) != 1
+        || hashlen != retlen
+        || EVP_PKEY_derive(pctx, notsecret, &retlen) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cc: notsecret", notsecret, hashlen);
+# endif
+    if (hashlen < OSSL_ECH_SIGNAL_LEN
+        || !tls13_hkdf_expand(s, md, notsecret,
+                              (const unsigned char *)label, labellen,
+                              hashval, hashlen, hoval,
+                              OSSL_ECH_SIGNAL_LEN, 1)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    rv = 1;
+err:
+    EVP_PKEY_CTX_free(pctx);
+    return rv;
+}
+
+/*
+ * ECH accept_confirmation calculation
+ * for_hrr is 1 if this is for an HRR, otherwise for SH
+ * acbuf is an 8 octet buffer for the confirmation value
+ * shlen is the server hello length
+ * return: 1 for success, 0 otherwise
+ *
+ * This is a magic value in the ServerHello.random lower 8 octets
+ * that is used to signal that the inner worked.
+ *
+ * As per spec:
+ *
+ * accept_confirmation = HKDF-Expand-Label(
+ *         HKDF-Extract(0, ClientHelloInner.random),
+ *         "ech accept confirmation",
+ *         transcript_ech_conf,
+ *         8)
+ *
+ * transcript_ech_conf = ClientHelloInner..ServerHello
+ *         with last 8 octets of ServerHello.random==0x00
+ *
+ * and with differences due to HRR
+ */
+int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
+                          unsigned char acbuf[OSSL_ECH_SIGNAL_LEN],
+                          const size_t shlen)
+{
+    int rv = 0;
+    EVP_MD_CTX *ctx = NULL;
+    EVP_MD *md = NULL;
+    unsigned char *tbuf = NULL, *conf_loc = NULL;
+    unsigned char *fixedshbuf = NULL;
+    size_t fixedshbuf_len = 0, tlen = 0, chend = 0;
+    /* shoffset is: 4 + 2 + 32 - 8 */
+    size_t shoffset = SSL3_HM_HEADER_LENGTH + sizeof(uint16_t)
+        + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN;
+    unsigned int hashlen = 0;
+    unsigned char hashval[EVP_MAX_MD_SIZE];
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+
+    if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
+        goto end;
+    }
+    if (ossl_ech_intbuf_fetch(s, &tbuf, &tlen) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
+        goto end;
+    }
+    chend = tlen - shlen - 4;
+    fixedshbuf_len = shlen + 4;
+    if (s->server) {
+        chend = tlen - shlen;
+        fixedshbuf_len = shlen;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cx: tbuf b4-b4", tbuf, tlen);
+# endif
+    /* put zeros in correct place */
+    if (for_hrr == 0) { /* zap magic octets at fixed place for SH */
+        conf_loc = tbuf + chend + shoffset;
+    } else {
+        if (s->server == 1) { /* we get to say where we put ECH:-) */
+            conf_loc = tbuf + tlen - OSSL_ECH_SIGNAL_LEN;
+        } else {
+            if (s->ext.ech.hrrsignal_p == NULL) {
+                /* No ECH found so we'll exit, but set random output */
+                if (RAND_bytes_ex(sctx->libctx, acbuf,
+                                  OSSL_ECH_SIGNAL_LEN, 0) <= 0) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
+                    goto end;
+                }
+                rv = 1;
+                goto end;
+            }
+            conf_loc = s->ext.ech.hrrsignal_p;
+        }
+    }
+    memset(conf_loc, 0, OSSL_ECH_SIGNAL_LEN);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cx: tbuf after", tbuf, tlen);
+# endif
+    if ((ctx = EVP_MD_CTX_new()) == NULL
+        || EVP_DigestInit_ex(ctx, md, NULL) <= 0
+        || EVP_DigestUpdate(ctx, tbuf, tlen) <= 0
+        || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto end;
+    }
+    EVP_MD_CTX_free(ctx);
+    ctx = NULL;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("cx: hashval", hashval, hashlen);
+# endif
+    /* calculate and set the final output */
+    if (ech_hkdf_extract_wrap(s, md, for_hrr, hashval, hashlen, acbuf) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto end;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    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);
+    /* 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);
+    if (s->server && s->hello_retry_request == SSL_HRR_COMPLETE)
+        ossl_ech_reset_hs_buffer(s, tbuf, tlen - fixedshbuf_len);
+    rv = 1;
+end:
+    OPENSSL_free(fixedshbuf);
+    EVP_MD_CTX_free(ctx);
+    return rv;
+}
+
+/*!
+ * Given a CH find the offsets of the session id, extensions and ECH
+ * pkt is the CH
+ * sessid_off points to offset of session_id length
+ * exts_off points to offset of extensions
+ * ech_off points to offset of ECH
+ * echtype points to the ext type of the ECH
+ * inner 1 if the ECH is marked as an inner, 0 for outer
+ * sni_off points to offset of (outer) SNI
+ * return 1 for success, other otherwise
+ *
+ * Offsets are set to zero if relevant thing not found.
+ * Offsets are returned to the type or length field in question.
+ *
+ * Note: input here is untrusted!
+ */
+int ossl_ech_get_ch_offsets(SSL_CONNECTION *s, PACKET *pkt, size_t *sessid_off,
+                            size_t *exts_off, size_t *ech_off, uint16_t *echtype,
+                            int *inner, size_t *sni_off)
+{
+    const unsigned char *ch = NULL;
+    size_t ch_len = 0, exts_len = 0, sni_len = 0, ech_len = 0;
+
+    if (s == NULL || pkt == NULL || sessid_off == NULL || exts_off == NULL
+        || ech_off == NULL || echtype == NULL || inner == NULL
+        || sni_off == NULL) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    /* check if we've already done the work */
+    if (s->ext.ech.ch_offsets_done == 1) {
+        *sessid_off = s->ext.ech.sessid_off;
+        *exts_off = s->ext.ech.exts_off;
+        *ech_off = s->ext.ech.ech_off;
+        *echtype = s->ext.ech.echtype;
+        *inner = s->ext.ech.inner;
+        *sni_off = s->ext.ech.sni_off;
+        return 1;
+    }
+    *sessid_off = 0;
+    *exts_off = 0;
+    *ech_off = 0;
+    *echtype = OSSL_ECH_type_unknown;
+    *sni_off = 0;
+    /* do the work */
+    ch_len = PACKET_remaining(pkt);
+    if (PACKET_peek_bytes(pkt, &ch, ch_len) != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    if (ossl_ech_helper_get_ch_offsets(ch, ch_len, sessid_off, exts_off,
+                                       &exts_len, ech_off, echtype, &ech_len,
+                                       sni_off, &sni_len, inner) != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "orig CH/ECH type: %4x\n", *echtype);
+    } OSSL_TRACE_END(TLS);
+    ossl_ech_pbuf("orig CH", (unsigned char *)ch, ch_len);
+    ossl_ech_pbuf("orig CH exts", (unsigned char *)ch + *exts_off, exts_len);
+    ossl_ech_pbuf("orig CH/ECH", (unsigned char *)ch + *ech_off, ech_len);
+    ossl_ech_pbuf("orig CH SNI", (unsigned char *)ch + *sni_off, sni_len);
+# endif
+    s->ext.ech.sessid_off = *sessid_off;
+    s->ext.ech.exts_off = *exts_off;
+    s->ext.ech.ech_off = *ech_off;
+    s->ext.ech.echtype = *echtype;
+    s->ext.ech.inner = *inner;
+    s->ext.ech.sni_off = *sni_off;
+    s->ext.ech.ch_offsets_done = 1;
+    return 1;
+}
+
+static void ossl_ech_encch_free(OSSL_ECH_ENCCH *tbf)
+{
+    if (tbf == NULL)
+        return;
+    OPENSSL_free(tbf->enc);
+    OPENSSL_free(tbf->payload);
+    return;
+}
+
+/*
+ * decode outer sni value so we can trace it
+ * osni_str is the string-form of the SNI
+ * opd is the outer CH buffer
+ * opl is the length of the above
+ * snioffset is where we find the outer SNI
+ *
+ * The caller doesn't have to free the osni_str.
+ */
+static int ech_get_outer_sni(SSL_CONNECTION *s, char **osni_str,
+                             const unsigned char *opd, size_t opl,
+                             size_t snioffset)
+{
+    PACKET wrap, osni;
+    unsigned int type, osnilen;
+
+    if (snioffset >= opl
+        || !PACKET_buf_init(&wrap, opd + snioffset, opl - snioffset)
+        || !PACKET_get_net_2(&wrap, &type)
+        || type != 0
+        || !PACKET_get_net_2(&wrap, &osnilen)
+        || !PACKET_get_sub_packet(&wrap, &osni, osnilen)
+        || tls_parse_ctos_server_name(s, &osni, 0, NULL, 0) != 1)
+        return 0;
+    OPENSSL_free(s->ext.ech.outer_hostname);
+    *osni_str = s->ext.ech.outer_hostname = s->ext.hostname;
+    /* clean up what the ECH-unaware parse func above left behind */
+    s->ext.hostname = NULL;
+    s->servername_done = 0;
+    return 1;
+}
+
+/*
+ * decode EncryptedClientHello extension value
+ * pkt contains the ECH value as a PACKET
+ * retext is the returned decoded structure
+ * payload_offset is the offset to the ciphertext
+ * return 1 for good, 0 for bad
+ *
+ * SSLfatal called from inside, as needed
+ */
+static int ech_decode_inbound_ech(SSL_CONNECTION *s, PACKET *pkt,
+                                  OSSL_ECH_ENCCH **retext,
+                                  size_t *payload_offset)
+{
+    unsigned int innerorouter = 0xff;
+    unsigned int pval_tmp; /* tmp placeholder of value from packet */
+    OSSL_ECH_ENCCH *extval = NULL;
+    const unsigned char *startofech = NULL;
+
+    /*
+     * Decode the inbound ECH value.
+     *  enum { outer(0), inner(1) } ECHClientHelloType;
+     *  struct {
+     *     ECHClientHelloType type;
+     *     select (ECHClientHello.type) {
+     *         case outer:
+     *             HpkeSymmetricCipherSuite cipher_suite;
+     *             uint8 config_id;
+     *             opaque enc<0..2^16-1>;
+     *             opaque payload<1..2^16-1>;
+     *         case inner:
+     *             Empty;
+     *     };
+     *  } ECHClientHello;
+     */
+    startofech = PACKET_data(pkt);
+    extval = OPENSSL_zalloc(sizeof(OSSL_ECH_ENCCH));
+    if (extval == NULL)
+        goto err;
+    if (!PACKET_get_1(pkt, &innerorouter)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (innerorouter != OSSL_ECH_OUTER_CH_TYPE) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (!PACKET_get_net_2(pkt, &pval_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    extval->kdf_id = pval_tmp & 0xffff;
+    if (!PACKET_get_net_2(pkt, &pval_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    extval->aead_id = pval_tmp & 0xffff;
+    /* config id */
+    if (!PACKET_copy_bytes(pkt, &extval->config_id, 1)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EARLY config id", &extval->config_id, 1);
+# endif
+    s->ext.ech.attempted_cid = extval->config_id;
+    /* enc - the client's public share */
+    if (!PACKET_get_net_2(pkt, &pval_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (pval_tmp > OSSL_ECH_MAX_GREASE_PUB) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (pval_tmp > PACKET_remaining(pkt)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (pval_tmp == 0 && s->hello_retry_request != SSL_HRR_PENDING) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    } else if (pval_tmp > 0 && s->hello_retry_request == SSL_HRR_PENDING) {
+        unsigned char *tmpenc = NULL;
+
+        /*
+         * if doing HRR, client should only send this when GREASEing
+         * and it should be the same value as 1st time, so we'll check
+         * that
+         */
+        if (s->ext.ech.pub == NULL || s->ext.ech.pub_len == 0) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (pval_tmp != s->ext.ech.pub_len) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        tmpenc = OPENSSL_malloc(pval_tmp);
+        if (tmpenc == NULL)
+            goto err;
+        if (!PACKET_copy_bytes(pkt, tmpenc, pval_tmp)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (memcmp(tmpenc, s->ext.ech.pub, pval_tmp) != 0) {
+            OPENSSL_free(tmpenc);
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        OPENSSL_free(tmpenc);
+    } else if (pval_tmp == 0 && s->hello_retry_request == SSL_HRR_PENDING) {
+        if (s->ext.ech.pub == NULL || s->ext.ech.pub_len == 0) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        extval->enc_len = s->ext.ech.pub_len;
+        extval->enc = OPENSSL_malloc(extval->enc_len);
+        if (extval->enc == NULL)
+            goto err;
+        memcpy(extval->enc, s->ext.ech.pub, extval->enc_len);
+    } else {
+        extval->enc_len = pval_tmp;
+        extval->enc = OPENSSL_malloc(pval_tmp);
+        if (extval->enc == NULL)
+            goto err;
+        if (!PACKET_copy_bytes(pkt, extval->enc, pval_tmp)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        /* squirrel away that value in case of future HRR */
+        OPENSSL_free(s->ext.ech.pub);
+        s->ext.ech.pub_len = extval->enc_len;
+        s->ext.ech.pub = OPENSSL_malloc(extval->enc_len);
+        if (s->ext.ech.pub == NULL)
+            goto err;
+        memcpy(s->ext.ech.pub, extval->enc, extval->enc_len);
+    }
+    /* payload - the encrypted CH */
+    *payload_offset = PACKET_data(pkt) - startofech;
+    if (!PACKET_get_net_2(pkt, &pval_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (pval_tmp > OSSL_ECH_MAX_PAYLOAD_LEN) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (pval_tmp > PACKET_remaining(pkt)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    extval->payload_len = pval_tmp;
+    extval->payload = OPENSSL_malloc(pval_tmp);
+    if (extval->payload == NULL)
+        goto err;
+    if (!PACKET_copy_bytes(pkt, extval->payload, pval_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    *retext = extval;
+    return 1;
+err:
+    if (extval != NULL) {
+        ossl_ech_encch_free(extval);
+        OPENSSL_free(extval);
+        extval = NULL;
+    }
+    return 0;
+}
+
+/*
+ * find outers if any, and do initial checks
+ * pkt is the encoded inner
+ * outers is the array of outer ext types
+ * n_outers is the number of outers found
+ * return 1 for good, 0 for error
+ *
+ * recall we're dealing with recovered ECH plaintext here so
+ * the content must be a TLSv1.3 ECH encoded inner
+ */
+static int ech_find_outers(SSL_CONNECTION *s, PACKET *pkt,
+                           uint16_t *outers, size_t *n_outers)
+{
+    const unsigned char *pp_tmp;
+    unsigned int pi_tmp, extlens, etype, elen, olen;
+    int outers_found = 0;
+    size_t i;
+    PACKET op;
+
+    PACKET_null_init(&op);
+    /* chew up the packet to extensions */
+    if (!PACKET_get_net_2(pkt, &pi_tmp)
+        || pi_tmp != TLS1_2_VERSION
+        || !PACKET_get_bytes(pkt, &pp_tmp, SSL3_RANDOM_SIZE)
+        || !PACKET_get_1(pkt, &pi_tmp)
+        || pi_tmp != 0x00 /* zero'd session id */
+        || !PACKET_get_net_2(pkt, &pi_tmp) /* ciphersuite len */
+        || !PACKET_get_bytes(pkt, &pp_tmp, pi_tmp) /* suites */
+        || !PACKET_get_1(pkt, &pi_tmp) /* compression meths */
+        || pi_tmp != 0x01 /* 1 octet of comressions */
+        || !PACKET_get_1(pkt, &pi_tmp) /* compression meths */
+        || pi_tmp != 0x00 /* 1 octet of no comressions */
+        || !PACKET_get_net_2(pkt, &extlens) /* len(extensions) */
+        || extlens == 0) { /* no extensions! */
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    while (PACKET_remaining(pkt) > 0 && outers_found == 0) {
+        if (!PACKET_get_net_2(pkt, &etype)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (etype == TLSEXT_TYPE_outer_extensions) {
+            outers_found = 1;
+            if (!PACKET_get_length_prefixed_2(pkt, &op)) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+                goto err;
+            }
+        } else { /* skip over */
+            if (!PACKET_get_net_2(pkt, &elen)
+                || !PACKET_get_bytes(pkt, &pp_tmp, elen)) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+                goto err;
+            }
+        }
+    }
+
+    if (outers_found == 0) { /* which is fine! */
+        *n_outers = 0;
+        return 1;
+    }
+    /*
+     * outers has a silly internal length as well and that better
+     * be one less than the extension length and an even number
+     * and we only support a certain max of outers
+     */
+    if (!PACKET_get_1(&op, &olen)
+        || olen % 2 == 1
+        || olen / 2 > OSSL_ECH_OUTERS_MAX) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    *n_outers = olen / 2;
+    for (i = 0; i != *n_outers; i++) {
+        if (!PACKET_get_net_2(&op, &pi_tmp)
+            || pi_tmp == TLSEXT_TYPE_outer_extensions) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        outers[i] = (uint16_t) pi_tmp;
+    }
+    return 1;
+err:
+    return 0;
+}
+
+/*
+ * copy one extension from outer to inner
+ * di is the reconstituted inner CH
+ * type2copy is the outer type to copy
+ * extsbuf is the outer extensions buffer
+ * extslen is the outer extensions buffer length
+ * return 1 for good 0 for error
+ */
+static int ech_copy_ext(SSL_CONNECTION *s, WPACKET *di, uint16_t type2copy,
+                        const unsigned char *extsbuf, size_t extslen)
+{
+    PACKET exts;
+    unsigned int etype, elen;
+    const unsigned char *eval;
+
+    if (PACKET_buf_init(&exts, extsbuf, extslen) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    while (PACKET_remaining(&exts) > 0) {
+        if (!PACKET_get_net_2(&exts, &etype)
+            || !PACKET_get_net_2(&exts, &elen)
+            || !PACKET_get_bytes(&exts, &eval, elen)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (etype == type2copy) {
+            if (!WPACKET_put_bytes_u16(di, etype)
+                || !WPACKET_put_bytes_u16(di, elen)
+                || !WPACKET_memcpy(di, eval, elen)) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+                goto err;
+            }
+            return 1;
+        }
+    }
+    /* we didn't find such an extension - that's an error */
+    SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+err:
+    return 0;
+}
+
+/*
+ * reconstitute the inner CH from encoded inner and outers
+ * di is the reconstituted inner CH
+ * ei is the encoded inner
+ * ob is the outer CH as a buffer
+ * ob_len is the size of the above
+ * outers is the array of outer ext types
+ * n_outers is the number of outers found
+ * return 1 for good, 0 for error
+ */
+static int ech_reconstitute_inner(SSL_CONNECTION *s, WPACKET *di, PACKET *ei,
+                                  const unsigned char *ob, size_t ob_len,
+                                  uint16_t *outers, size_t n_outers)
+{
+    const unsigned char *pp_tmp, *eval, *outer_exts;
+    unsigned int pi_tmp, etype, elen, outer_extslen;
+    PACKET outer, session_id;
+    size_t i;
+
+    if (PACKET_buf_init(&outer, ob, ob_len) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* read/write from encoded inner to decoded inner with help from outer */
+    if (/* version */
+        !PACKET_get_net_2(&outer, &pi_tmp)
+        || !PACKET_get_net_2(ei, &pi_tmp)
+        || !WPACKET_put_bytes_u16(di, pi_tmp)
+
+        /* client random */
+        || !PACKET_get_bytes(&outer, &pp_tmp, SSL3_RANDOM_SIZE)
+        || !PACKET_get_bytes(ei, &pp_tmp, SSL3_RANDOM_SIZE)
+        || !WPACKET_memcpy(di, pp_tmp, SSL3_RANDOM_SIZE)
+
+        /* session ID */
+        || !PACKET_get_1(ei, &pi_tmp)
+        || !PACKET_get_length_prefixed_1(&outer, &session_id)
+        || !WPACKET_start_sub_packet_u8(di)
+        || (PACKET_remaining(&session_id) != 0
+            && !WPACKET_memcpy(di, PACKET_data(&session_id),
+                               PACKET_remaining(&session_id)))
+        || !WPACKET_close(di)
+
+        /* ciphersuites */
+        || !PACKET_get_net_2(&outer, &pi_tmp) /* ciphersuite len */
+        || !PACKET_get_bytes(&outer, &pp_tmp, pi_tmp) /* suites */
+        || !PACKET_get_net_2(ei, &pi_tmp) /* ciphersuite len */
+        || !PACKET_get_bytes(ei, &pp_tmp, pi_tmp) /* suites */
+        || !WPACKET_put_bytes_u16(di, pi_tmp)
+        || !WPACKET_memcpy(di, pp_tmp, pi_tmp)
+
+        /* compression len & meth */
+        || !PACKET_get_net_2(ei, &pi_tmp)
+        || !PACKET_get_net_2(&outer, &pi_tmp)
+        || !WPACKET_put_bytes_u16(di, pi_tmp)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    /* handle simple, but unlikely, case first */
+    if (n_outers == 0) {
+        if (PACKET_remaining(ei) == 0)
+            return 1; /* no exts is theoretically possible */
+        if (!PACKET_get_net_2(ei, &pi_tmp) /* len(extensions) */
+            || !PACKET_get_bytes(ei, &pp_tmp, pi_tmp)
+            || !WPACKET_put_bytes_u16(di, pi_tmp)
+            || !WPACKET_memcpy(di, pp_tmp, pi_tmp)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        WPACKET_close(di);
+        return 1;
+    }
+    /*
+     * general case, copy one by one from inner, 'till we hit
+     * the outers extension, then copy one by one from outer
+     */
+    if (!PACKET_get_net_2(ei, &pi_tmp) /* len(extensions) */
+        || !PACKET_get_net_2(&outer, &outer_extslen)
+        || !PACKET_get_bytes(&outer, &outer_exts, outer_extslen)
+        || !WPACKET_start_sub_packet_u16(di)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    while (PACKET_remaining(ei) > 0) {
+        if (!PACKET_get_net_2(ei, &etype)
+            || !PACKET_get_net_2(ei, &elen)
+            || !PACKET_get_bytes(ei, &eval, elen)) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (etype == TLSEXT_TYPE_outer_extensions) {
+            for (i = 0; i != n_outers; i++) {
+                if (ech_copy_ext(s, di, outers[i],
+                                 outer_exts, outer_extslen) != 1)
+                    /* SSLfatal called already */
+                    goto err;
+            }
+        } else {
+            if (!WPACKET_put_bytes_u16(di, etype)
+                || !WPACKET_put_bytes_u16(di, elen)
+                || !WPACKET_memcpy(di, eval, elen)) {
+                SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+                goto err;
+            }
+        }
+    }
+    WPACKET_close(di);
+    return 1;
+err:
+    WPACKET_cleanup(di);
+    return 0;
+}
+
+/*
+ * After successful ECH decrypt, we decode, decompress etc.
+ * ob is the outer CH as a buffer
+ * ob_len is the size of the above
+ * return 1 for success, error otherwise
+ *
+ * We need the outer CH as a buffer (ob, below) so we can
+ * ECH-decompress.
+ * The plaintext we start from is in encoded_innerch
+ * and our final decoded, decompressed buffer will end up
+ * in innerch (which'll then be further processed).
+ * That further processing includes all existing decoding
+ * checks so we should be fine wrt fuzzing without having
+ * to make all checks here (e.g. we can assume that the
+ * protocol version, NULL compression etc are correct here -
+ * if not, those'll be caught later).
+ * Note: there are a lot of literal values here, but it's
+ * not clear that changing those to #define'd symbols will
+ * help much - a change to the length of a type or from a
+ * 2 octet length to longer would seem unlikely.
+ */
+static int ech_decode_inner(SSL_CONNECTION *s, const unsigned char *ob,
+                            size_t ob_len, unsigned char *encoded_inner,
+                            size_t encoded_inner_len)
+{
+    int rv = 0;
+    PACKET ei; /* encoded inner */
+    BUF_MEM *di_mem = NULL;
+    uint16_t outers[OSSL_ECH_OUTERS_MAX]; /* compressed extension types */
+    size_t n_outers = 0;
+    WPACKET di;
+
+    if (encoded_inner == NULL || ob == NULL || ob_len == 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if ((di_mem = BUF_MEM_new()) == NULL
+        || !BUF_MEM_grow(di_mem, SSL3_RT_MAX_PLAIN_LENGTH)
+        || !WPACKET_init(&di, di_mem)
+        || !WPACKET_put_bytes_u8(&di, SSL3_MT_CLIENT_HELLO)
+        || !WPACKET_start_sub_packet_u24(&di)
+        || !PACKET_buf_init(&ei, encoded_inner, encoded_inner_len)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    memset(outers, -1, sizeof(outers)); /* fill with known values for debug */
+# endif
+
+    /* 1. check for outers and make initial checks of those */
+    if (ech_find_outers(s, &ei, outers, &n_outers) != 1)
+        goto err; /* SSLfatal called already */
+
+    /* 2. reconstitute inner CH */
+    /* reset ei */
+    if (PACKET_buf_init(&ei, encoded_inner, encoded_inner_len) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (ech_reconstitute_inner(s, &di, &ei, ob, ob_len, outers, n_outers) != 1)
+        goto err; /* SSLfatal called already */
+    /* 3. store final inner CH in connection */
+    WPACKET_close(&di);
+    if (!WPACKET_get_length(&di, &s->ext.ech.innerch_len)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    OPENSSL_free(s->ext.ech.innerch);
+    s->ext.ech.innerch = (unsigned char *)di_mem->data;
+    di_mem->data = NULL;
+    rv = 1;
+err:
+    WPACKET_cleanup(&di);
+    BUF_MEM_free(di_mem);
+    return rv;
+}
+
+/*
+ * wrapper for hpke_dec just to save code repetition
+ * ee is the selected ECH_STORE entry
+ * the_ech is the value sent by the client
+ * aad_len is the length of the AAD to use
+ * aad is the AAD to use
+ * forhrr is 0 if not hrr, 1 if this is for 2nd CH
+ * innerlen points to the size of the recovered plaintext
+ * return pointer to plaintext or NULL (if error)
+ *
+ * The plaintext returned is allocated here and must
+ * be freed by the caller later.
+ */
+static unsigned char *hpke_decrypt_encch(SSL_CONNECTION *s,
+                                         OSSL_ECHSTORE_ENTRY *ee,
+                                         OSSL_ECH_ENCCH *the_ech,
+                                         size_t aad_len, unsigned char *aad,
+                                         int forhrr, size_t *innerlen)
+{
+    size_t cipherlen = 0;
+    unsigned char *cipher = NULL;
+    size_t senderpublen = 0;
+    unsigned char *senderpub = NULL;
+    size_t clearlen = 0;
+    unsigned char *clear = NULL;
+    int hpke_mode = OSSL_HPKE_MODE_BASE;
+    OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+    unsigned char info[OSSL_ECH_MAX_INFO_LEN];
+    size_t info_len = OSSL_ECH_MAX_INFO_LEN;
+    int rv = 0;
+    OSSL_HPKE_CTX *hctx = NULL;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    size_t publen = 0;
+    unsigned char *pub = NULL;
+# endif
+
+    if (ee == NULL || ee->nsuites == 0)
+        return NULL;
+    cipherlen = the_ech->payload_len;
+    cipher = the_ech->payload;
+    senderpublen = the_ech->enc_len;
+    senderpub = the_ech->enc;
+    hpke_suite.aead_id = the_ech->aead_id;
+    hpke_suite.kdf_id = the_ech->kdf_id;
+    clearlen = cipherlen; /* small overestimate */
+    clear = OPENSSL_malloc(clearlen);
+    if (clear == NULL)
+        return NULL;
+    /* The kem_id will be the same for all suites in the entry */
+    hpke_suite.kem_id = ee->suites[0].kem_id;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    publen = ee->pub_len;
+    pub = ee->pub;
+    ossl_ech_pbuf("aad", aad, aad_len);
+    ossl_ech_pbuf("my local pub", pub, publen);
+    ossl_ech_pbuf("senderpub", senderpub, senderpublen);
+    ossl_ech_pbuf("cipher", cipher, cipherlen);
+# endif
+    if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len,
+                               info, &info_len) != 1) {
+        OPENSSL_free(clear);
+        return NULL;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("info", info, info_len);
+# endif
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out,
+                   "hpke_dec suite: kem: %04x, kdf: %04x, aead: %04x\n",
+                   hpke_suite.kem_id, hpke_suite.kdf_id, hpke_suite.aead_id);
+    } OSSL_TRACE_END(TLS);
+    /*
+     * We may generate externally visible OpenSSL errors
+     * if decryption fails (which is normal) but we'll
+     * ignore those as we might be dealing with a GREASEd
+     * ECH. To do that we need to now ignore some errors
+     * so we use ERR_set_mark() then later ERR_pop_to_mark().
+     */
+    ERR_set_mark();
+    /* Use OSSL_HPKE_* APIs */
+    hctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, OSSL_HPKE_ROLE_RECEIVER,
+                             sctx->libctx, sctx->propq);
+    if (hctx == NULL)
+        goto clearerrs;
+    rv = OSSL_HPKE_decap(hctx, senderpub, senderpublen, ee->keyshare,
+                         info, info_len);
+    if (rv != 1)
+        goto clearerrs;
+    if (forhrr == 1) {
+        rv = OSSL_HPKE_CTX_set_seq(hctx, 1);
+        if (rv != 1) {
+            /* don't clear this error - GREASE can't cause it */
+            ERR_clear_last_mark();
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto end;
+        }
+    }
+    rv = OSSL_HPKE_open(hctx, clear, &clearlen, aad, aad_len,
+                        cipher, cipherlen);
+clearerrs:
+    /* close off our error handling */
+    ERR_pop_to_mark();
+end:
+    OSSL_HPKE_CTX_free(hctx);
+    if (rv != 1) {
+        OSSL_TRACE(TLS, "HPKE decryption failed somehow\n");
+        OPENSSL_free(clear);
+        return NULL;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("padded clear", clear, clearlen);
+# endif
+    /* we need to remove possible (actually, v. likely) padding */
+    *innerlen = clearlen;
+    if (ee->version == OSSL_ECH_RFCXXXX_VERSION) {
+        /* draft-13 pads after the encoded CH with zeros */
+        size_t extsoffset = 0;
+        size_t extslen = 0;
+        size_t ch_len = 0;
+        size_t startofsessid = 0;
+        size_t echoffset = 0; /* offset of start of ECH within CH */
+        uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */
+        size_t outersnioffset = 0; /* offset to SNI in outer */
+        int innerflag = -1;
+        PACKET innerchpkt;
+
+        if (PACKET_buf_init(&innerchpkt, clear, clearlen) != 1) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto paderr;
+        }
+        /* reset the offsets, as we move from outer to inner CH */
+        s->ext.ech.ch_offsets_done = 0;
+        rv = ossl_ech_get_ch_offsets(s, &innerchpkt, &startofsessid,
+                                     &extsoffset, &echoffset, &echtype,
+                                     &innerflag, &outersnioffset);
+        if (rv != 1) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto paderr;
+        }
+        /* odd form of check below just for emphasis */
+        if ((extsoffset + 1) > clearlen) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto paderr;
+        }
+        extslen = (unsigned char)(clear[extsoffset]) * 256
+            + (unsigned char)(clear[extsoffset + 1]);
+        ch_len = extsoffset + 2 + extslen;
+        /* the check below protects us from bogus data */
+        if (ch_len > clearlen) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto paderr;
+        }
+        /*
+         * The RFC calls for that padding to be all zeros. I'm not so
+         * keen on that being a good idea to enforce, so we'll make it
+         * easy to not do so (but check by default)
+         */
+# define CHECKZEROS
+# ifdef CHECKZEROS
+        {
+            size_t zind = 0;
+
+            if (*innerlen < ch_len)
+                goto paderr;
+            for (zind = ch_len; zind != *innerlen; zind++) {
+                if (clear[zind] != 0x00)
+                    goto paderr;
+            }
+        }
+# endif
+        *innerlen = ch_len;
+# ifdef OSSL_ECH_SUPERVERBOSE
+        ossl_ech_pbuf("unpadded clear", clear, *innerlen);
+# endif
+        return clear;
+    }
+paderr:
+    OPENSSL_free(clear);
+    return NULL;
+}
+
+/*
+ * If an ECH is present, attempt decryption
+ * outerpkt is the packet with the outer CH
+ * newpkt is the packet with the decrypted inner CH
+ * return 1 for success, other otherwise
+ *
+ * If decryption succeeds, the caller can swap the inner and outer
+ * CHs so that all further processing will only take into account
+ * the inner CH.
+ *
+ * The fact that decryption worked is signalled to the caller
+ * via s->ext.ech.success
+ *
+ * This function is called early, (hence the name:-), before
+ * the outer CH decoding has really started, so we need to be
+ * careful peeking into the packet
+ *
+ * The plan:
+ * 1. check if there's an ECH
+ * 2. trial-decrypt or check if config matches one loaded
+ * 3. if decrypt fails tee-up GREASE
+ * 4. if decrypt worked, decode and de-compress cleartext to
+ *    make up real inner CH for later processing
+ */
+int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt)
+{
+    int num = 0, cfgind = -1, foundcfg = 0, forhrr = 0, innerflag = -1;
+    OSSL_ECH_ENCCH *extval = NULL;
+    PACKET echpkt;
+    const unsigned char *startofech = NULL, *opd = NULL;
+    size_t echlen = 0, clearlen = 0, aad_len = 0;
+    unsigned char *clear = NULL, *aad = NULL;
+    /* offsets of things within CH */
+    size_t startofsessid = 0, startofexts = 0, echoffset = 0, opl = 0;
+    size_t outersnioffset = 0, startofciphertext = 0, lenofciphertext = 0;
+    uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */
+    char *osni_str = NULL;
+    OSSL_ECHSTORE *es = NULL;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    if (s == NULL)
+        return 0;
+    if (outerpkt == NULL || newpkt == NULL) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    /* find offsets - on success, outputs are safe to use */
+    if (ossl_ech_get_ch_offsets(s, outerpkt, &startofsessid, &startofexts,
+                                &echoffset, &echtype, &innerflag,
+                                &outersnioffset) != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    if (echoffset == 0 || echtype != TLSEXT_TYPE_ech)
+        return 1; /* ECH not present or wrong version */
+    if (innerflag == 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    s->ext.ech.attempted = 1; /* Remember that we got an ECH */
+    s->ext.ech.attempted_type = echtype;
+    if (s->hello_retry_request == SSL_HRR_PENDING)
+        forhrr = 1; /* set forhrr if that's correct */
+    opl = PACKET_remaining(outerpkt);
+    opd = PACKET_data(outerpkt);
+    s->tmp_session_id_len = opd[startofsessid]; /* grab the session id */
+    if (s->tmp_session_id_len > SSL_MAX_SSL_SESSION_ID_LENGTH
+        || startofsessid + 1 + s->tmp_session_id_len > opl) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    memcpy(s->tmp_session_id, &opd[startofsessid + 1], s->tmp_session_id_len);
+    if (outersnioffset > 0) { /* Grab the outer SNI for tracing */
+        if (ech_get_outer_sni(s, &osni_str, opd, opl, outersnioffset) != 1
+            || osni_str == NULL) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        OSSL_TRACE1(TLS, "EARLY: outer SNI of %s\n", osni_str);
+    } else {
+        OSSL_TRACE(TLS, "EARLY: no sign of an outer SNI\n");
+    }
+    if (echoffset > opl - 4) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    startofech = &opd[echoffset + 4];
+    echlen = opd[echoffset + 2] * 256 + opd[echoffset + 3];
+    if (echlen > opl - echoffset - 4) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (PACKET_buf_init(&echpkt, startofech, echlen) != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    if (ech_decode_inbound_ech(s, &echpkt, &extval, &startofciphertext) != 1)
+        goto err; /* SSLfatal already called if needed */
+    /*
+     * startofciphertext is within the ECH value and after the length of the
+     * ciphertext, so we need to bump it by the offset of ECH within the CH
+     * plus the ECH type (2 octets) and length (also 2 octets) and that
+     * ciphertext length (another 2 octets) for a total of 6 octets
+     */
+    startofciphertext += echoffset + 6;
+    lenofciphertext = extval->payload_len;
+    aad_len = opl;
+    if (aad_len < startofciphertext + lenofciphertext) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    aad = OPENSSL_memdup(opd, aad_len);
+    if (aad == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    memset(aad + startofciphertext, 0, lenofciphertext);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EARLY aad", aad, aad_len);
+# endif
+    s->ext.ech.grease = OSSL_ECH_GREASE_UNKNOWN;
+    if (s->ext.ech.es == NULL) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    es = s->ext.ech.es;
+    num = (es == NULL || es->entries == NULL ? 0
+           : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    for (cfgind = 0; cfgind != num; cfgind++) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cfgind);
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out,
+                       "EARLY: rx'd config id (%x) ==? %d-th configured (%x)\n",
+                       extval->config_id, cfgind, ee->config_id);
+        } OSSL_TRACE_END(TLS);
+        if (extval->config_id == ee->config_id) {
+            foundcfg = 1;
+            break;
+        }
+    }
+    if (foundcfg == 1) {
+        clear = hpke_decrypt_encch(s, ee, extval, aad_len, aad,
+                                   forhrr, &clearlen);
+        if (clear == NULL)
+            s->ext.ech.grease = OSSL_ECH_IS_GREASE;
+    }
+    /* if still needed, trial decryptions */
+    if (clear == NULL && (s->options & SSL_OP_ECH_TRIALDECRYPT)) {
+        foundcfg = 0; /* reset as we're trying again */
+        for (cfgind = 0; cfgind != num; cfgind++) {
+            ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cfgind);
+            clear = hpke_decrypt_encch(s, ee, extval,
+                                       aad_len, aad, forhrr, &clearlen);
+            if (clear != NULL) {
+                foundcfg = 1;
+                s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
+                break;
+            }
+        }
+    }
+    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.success = 0;
+    if (clear != NULL) {
+        s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
+        s->ext.ech.success = 1;
+    }
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "EARLY: success: %d, assume_grease: %d, "
+                   "foundcfg: %d, cfgind: %d, clearlen: %zd, clear %p\n",
+                   s->ext.ech.success, s->ext.ech.grease, foundcfg,
+                   cfgind, clearlen, (void *)clear);
+    } OSSL_TRACE_END(TLS);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    if (foundcfg == 1 && clear != NULL) { /* Bit more logging */
+        ossl_ech_pbuf("local config_id", &ee->config_id, 1);
+        ossl_ech_pbuf("remote config_id", &extval->config_id, 1);
+        ossl_ech_pbuf("clear", clear, clearlen);
+    }
+# endif
+    if (extval != NULL) {
+        ossl_ech_encch_free(extval);
+        OPENSSL_free(extval);
+        extval = NULL;
+    }
+    if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) {
+        OPENSSL_free(clear);
+        return 1;
+    }
+    /* 4. if decrypt worked, de-compress cleartext to make up real inner CH */
+    if (ech_decode_inner(s, opd, opl, clear, clearlen) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    OPENSSL_free(clear);
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("Inner CH (decoded)", s->ext.ech.innerch,
+                  s->ext.ech.innerch_len);
+# endif
+    if (PACKET_buf_init(newpkt, s->ext.ech.innerch,
+                        s->ext.ech.innerch_len) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* tls_process_client_hello doesn't want the message header, so skip it */
+    if (!PACKET_forward(newpkt, SSL3_HM_HEADER_LENGTH)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (ossl_ech_intbuf_add(s, s->ext.ech.innerch,
+                            s->ext.ech.innerch_len, 0) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    return 1;
+err:
+    OPENSSL_free(aad);
+    if (extval != NULL) {
+        ossl_ech_encch_free(extval);
+        OPENSSL_free(extval);
+    }
+    OPENSSL_free(clear);
+    return 0;
+}
+
+int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf,
+                        size_t blen, int hash_existing)
+{
+    EVP_MD_CTX *ctx = NULL;
+    EVP_MD *md = NULL;
+    unsigned int rv = 0, hashlen = 0;
+    unsigned char hashval[EVP_MAX_MD_SIZE], *t1;
+    size_t tlen;
+    WPACKET tpkt = { 0 };
+    BUF_MEM *tpkt_mem = NULL;
+
+    if (s == NULL || buf == NULL || blen == 0)
+        goto err;
+    if (hash_existing == 1) {
+        /* hash existing buffer, needed during HRR */
+        if (s->ext.ech.transbuf == NULL
+            || (md = (EVP_MD *)ssl_handshake_md(s)) == NULL
+            || (ctx = EVP_MD_CTX_new()) == NULL
+            || EVP_DigestInit_ex(ctx, md, NULL) <= 0
+            || EVP_DigestUpdate(ctx, s->ext.ech.transbuf,
+                                s->ext.ech.transbuf_len) <= 0
+            || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0
+            || (tpkt_mem = BUF_MEM_new()) == NULL
+            || !WPACKET_init(&tpkt, tpkt_mem)
+            || !WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH)
+            || !WPACKET_put_bytes_u24(&tpkt, hashlen)
+            || !WPACKET_memcpy(&tpkt, hashval, hashlen)
+            || !WPACKET_get_length(&tpkt, &tlen)
+            || (t1 = OPENSSL_realloc(s->ext.ech.transbuf, tlen + blen)) == NULL)
+            goto err;
+        s->ext.ech.transbuf = t1;
+        memcpy(s->ext.ech.transbuf, tpkt_mem->data, tlen);
+        memcpy(s->ext.ech.transbuf + tlen, buf, blen);
+        s->ext.ech.transbuf_len = tlen + blen;
+    } else {
+        /* just add new octets */
+        if ((t1 = OPENSSL_realloc(s->ext.ech.transbuf,
+                                  s->ext.ech.transbuf_len + blen)) == NULL)
+            goto err;
+        s->ext.ech.transbuf = t1;
+        memcpy(s->ext.ech.transbuf + s->ext.ech.transbuf_len, buf, blen);
+        s->ext.ech.transbuf_len += blen;
+    }
+    rv = 1;
+err:
+    BUF_MEM_free(tpkt_mem);
+    WPACKET_cleanup(&tpkt);
+    EVP_MD_CTX_free(ctx);
+    return rv;
+}
+
+int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen)
+{
+    if (s == NULL || buf == NULL || blen == NULL || s->ext.ech.transbuf == NULL)
+        return 0;
+    *buf = s->ext.ech.transbuf;
+    *blen = s->ext.ech.transbuf_len;
+    return 1;
+}
+
+int ossl_ech_stash_keyshares(SSL_CONNECTION *s)
+{
+    size_t i;
+
+    ech_free_stashed_key_shares(&s->ext.ech);
+    for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) {
+        s->ext.ech.ks_pkey[i] = s->s3.tmp.ks_pkey[i];
+        if (EVP_PKEY_up_ref(s->ext.ech.ks_pkey[i]) != 1)
+            return 0;
+        s->ext.ech.ks_group_id[i] = s->s3.tmp.ks_group_id[i];
+    }
+    s->ext.ech.num_ks_pkey = s->s3.tmp.num_ks_pkey;
+    return 1;
+}
+
+int ossl_ech_unstash_keyshares(SSL_CONNECTION *s)
+{
+    size_t i;
+
+    for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) {
+        EVP_PKEY_free(s->s3.tmp.ks_pkey[i]);
+        s->s3.tmp.ks_pkey[i] = NULL;
+    }
+    for (i = 0; i != s->ext.ech.num_ks_pkey; i++) {
+        s->s3.tmp.ks_pkey[i] = s->ext.ech.ks_pkey[i];
+        if (EVP_PKEY_up_ref(s->s3.tmp.ks_pkey[i]) != 1)
+            return 0;
+        s->s3.tmp.ks_group_id[i] = s->ext.ech.ks_group_id[i];
+    }
+    s->s3.tmp.num_ks_pkey = s->ext.ech.num_ks_pkey;
+    ech_free_stashed_key_shares(&s->ext.ech);
+    return 1;
+}
+#endif
diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h
new file mode 100644
index 0000000000000..9edf349de1c73
--- /dev/null
+++ b/ssl/ech/ech_local.h
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (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
+ */
+
+/*
+ * Internal data structures and prototypes for handling
+ * Encrypted ClientHello (ECH)
+ */
+#ifndef OPENSSL_NO_ECH
+
+# ifndef HEADER_ECH_LOCAL_H
+#  define HEADER_ECH_LOCAL_H
+
+#  include 
+#  include 
+#  include 
+
+/*
+ * Define this to get loads more lines of tracing which is
+ * very useful for interop.
+ * This needs tracing enabled at build time, e.g.:
+ *          $ ./config enable-ssl-trace enable-trace
+ * This added tracing will finally (mostly) disappear once the ECH RFC
+ * has issued, but is very useful for interop testing so some of it might
+ * be retained.
+ */
+#  define OSSL_ECH_SUPERVERBOSE
+
+/* values for s->ext.ech.grease */
+#  define OSSL_ECH_GREASE_UNKNOWN -1 /* when we're not yet sure */
+#  define OSSL_ECH_NOT_GREASE 0 /* when decryption worked */
+#  define OSSL_ECH_IS_GREASE 1 /* when decryption failed or GREASE wanted */
+
+/* value for uninitialised ECH version */
+#  define OSSL_ECH_type_unknown 0xffff
+/* value for not yet set ECH config_id */
+#  define OSSL_ECH_config_id_unset -1
+
+#  define OSSL_ECH_OUTER_CH_TYPE 0 /* outer ECHClientHello enum */
+#  define OSSL_ECH_INNER_CH_TYPE 1 /* inner ECHClientHello enum */
+
+#  define OSSL_ECH_CIPHER_LEN 4 /* ECHCipher length (2 for kdf, 2 for aead) */
+
+#  define OSSL_ECH_SIGNAL_LEN 8 /* length of ECH acceptance signal */
+
+/* size of string buffer returned via ECH callback */
+#  define OSSL_ECH_PBUF_SIZE 8 * 1024
+
+#  ifndef CLIENT_VERSION_LEN
+/*
+ * This is the legacy version length, i.e. len(0x0303). The same
+ * label is used in e.g. test/sslapitest.c and elsewhere but not
+ * defined in a header file I could find.
+ */
+#   define CLIENT_VERSION_LEN 2
+#  endif
+
+/*
+ * Reminder of what goes in DNS for ECH RFC XXXX
+ *
+ *     opaque HpkePublicKey<1..2^16-1>;
+ *     uint16 HpkeKemId;  // Defined in I-D.irtf-cfrg-hpke
+ *     uint16 HpkeKdfId;  // Defined in I-D.irtf-cfrg-hpke
+ *     uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke
+ *     struct {
+ *         HpkeKdfId kdf_id;
+ *         HpkeAeadId aead_id;
+ *     } HpkeSymmetricCipherSuite;
+ *     struct {
+ *         uint8 config_id;
+ *         HpkeKemId kem_id;
+ *         HpkePublicKey public_key;
+ *         HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>;
+ *     } HpkeKeyConfig;
+ *     struct {
+ *         HpkeKeyConfig key_config;
+ *         uint8 maximum_name_length;
+ *         opaque public_name<1..255>;
+ *         Extension extensions<0..2^16-1>;
+ *     } ECHConfigContents;
+ *     struct {
+ *         uint16 version;
+ *         uint16 length;
+ *         select (ECHConfig.version) {
+ *           case 0xfe0d: ECHConfigContents contents;
+ *         }
+ *     } ECHConfig;
+ *     ECHConfig ECHConfigList<1..2^16-1>;
+ */
+
+typedef struct ossl_echext_st {
+    uint16_t type;
+    uint16_t len;
+    unsigned char *val;
+} OSSL_ECHEXT;
+
+DEFINE_STACK_OF(OSSL_ECHEXT)
+
+typedef struct ossl_echstore_entry_st {
+    uint16_t version; /* 0xfe0d for RFC XXXX */
+    char *public_name;
+    size_t pub_len;
+    unsigned char *pub;
+    unsigned int nsuites;
+    OSSL_HPKE_SUITE *suites;
+    uint8_t max_name_length;
+    uint8_t config_id;
+    STACK_OF(OSSL_ECHEXT) *exts;
+    time_t loadtime; /* time public and private key were loaded from file */
+    EVP_PKEY *keyshare; /* long(ish) term ECH private keyshare on a server */
+    int for_retry; /* whether to use this ECHConfigList in a retry */
+    size_t encoded_len; /* length of overall encoded content */
+    unsigned char *encoded; /* overall encoded content */
+} OSSL_ECHSTORE_ENTRY;
+
+/*
+ * What we send in the ech CH extension:
+ *     enum { outer(0), inner(1) } ECHClientHelloType;
+ *     struct {
+ *        ECHClientHelloType type;
+ *        select (ECHClientHello.type) {
+ *            case outer:
+ *                HpkeSymmetricCipherSuite cipher_suite;
+ *                uint8 config_id;
+ *                opaque enc<0..2^16-1>;
+ *                opaque payload<1..2^16-1>;
+ *            case inner:
+ *                Empty;
+ *        };
+ *     } ECHClientHello;
+ *
+ */
+typedef struct ech_encch_st {
+    uint16_t kdf_id; /* ciphersuite  */
+    uint16_t aead_id; /* ciphersuite  */
+    uint8_t config_id; /* (maybe) identifies DNS RR value used */
+    size_t enc_len; /* public share */
+    unsigned char *enc; /* public share for sender */
+    size_t payload_len; /* ciphertext  */
+    unsigned char *payload; /* ciphertext  */
+} OSSL_ECH_ENCCH;
+
+DEFINE_STACK_OF(OSSL_ECHSTORE_ENTRY)
+
+struct ossl_echstore_st {
+    STACK_OF(OSSL_ECHSTORE_ENTRY) *entries;
+    OSSL_LIB_CTX *libctx;
+    char *propq;
+};
+
+/* ECH details associated with an SSL_CTX */
+typedef struct ossl_ech_ctx_st {
+    /* TODO(ECH): consider making es ref-counted */
+    OSSL_ECHSTORE *es;
+    unsigned char *alpn_outer;
+    size_t alpn_outer_len;
+    SSL_ech_cb_func cb; /* callback function for when ECH "done" */
+} OSSL_ECH_CTX;
+
+/* ECH details associated with an SSL_CONNECTION */
+typedef struct ossl_ech_conn_st {
+    /* TODO(ECH): consider making es ref-counted */
+    OSSL_ECHSTORE *es; /* ECHConfigList details */
+    int no_outer; /* set to 1 if we should send no outer SNI at all */
+    char *outer_hostname;
+    unsigned char *alpn_outer;
+    size_t alpn_outer_len;
+    SSL_ech_cb_func cb; /* callback function for when ECH "done" */
+    /*
+     * If ECH fails, then we switch to verifying the cert for the
+     * outer_hostname, meanwhile we still want to be able to trace
+     * the value we tried as the inner SNI for debug purposes
+     */
+    char *former_inner;
+    /* inner CH transcript buffer */
+    unsigned char *transbuf;
+    size_t transbuf_len;
+    /* inner ClientHello before ECH compression */
+    unsigned char *innerch;
+    size_t innerch_len;
+    /* encoded inner CH */
+    unsigned char *encoded_inner;
+    size_t encoded_inner_len;
+    /* lengths calculated early, used when encrypting at end of processing */
+    size_t clearlen;
+    size_t cipherlen;
+    /* location to put ciphertext, initially filled with zeros */
+    size_t cipher_offset;
+    /*
+     * Extensions are "outer-only" if the value is only sent in the
+     * outer CH and only the type is sent in the inner CH.
+     * We use this array to keep track of the extension types that
+     * have values only in the outer CH
+     * Currently, this is basically controlled at compile time, but
+     * in a way that could be varied, or, in future, put under
+     * run-time control, so having this isn't so much an overhead.
+     */
+    uint16_t outer_only[OSSL_ECH_OUTERS_MAX];
+    size_t n_outer_only; /* the number of outer_only extensions so far */
+    /*
+     * 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 */
+    int ch_depth; /* set during CH creation, 0: doing outer, 1: doing inner */
+    int attempted; /* 1 if ECH was or is being attempted, 0 otherwise */
+    int done; /* 1 if we've finished ECH calculations, 0 otherwise */
+    uint16_t attempted_type; /* ECH version used */
+    int attempted_cid; /* ECH config id sent/rx'd */
+    int backend; /* 1 if we're a server backend in split-mode, 0 otherwise */
+    /* When using a PSK stash the tick_identity from inner, for outer */
+    int tick_identity;
+    /*
+     * success is 1 if ECH succeeded, 0 otherwise, on the server this
+     * is known early, on the client we need to wait for the ECH confirm
+     * calculation based on the SH (or 2nd SH in case of HRR)
+     */
+    int success;
+    int grease; /* 1 if we're GREASEing, 0 otherwise */
+    char *grease_suite; /* HPKE suite string for GREASEing */
+    unsigned char *sent; /* GREASEy ECH value sent, in case needed for re-tx */
+    size_t sent_len;
+    unsigned char *returned; /* binary ECHConfigList retry-configs value */
+    size_t returned_len;
+    unsigned char *pub; /* client ephemeral public kept by server in case HRR */
+    size_t pub_len;
+    OSSL_HPKE_CTX *hpke_ctx; /* HPKE context, needed for HRR */
+    /*
+     * Offsets of various things we need to know about in an inbound
+     * ClientHello (CH) plus the type of ECH and whether that CH is an inner or
+     * outer CH. We find these once for the outer CH, by roughly parsing the CH
+     * so store them for later re-use. We need to re-do this parsing when we
+     * get the 2nd CH in the case of HRR, and when we move to processing the
+     * inner CH after successful ECH decyption, so we have a flag to say if
+     * we've done the work or not.
+     */
+    int ch_offsets_done;
+    size_t sessid_off; /* offset of session_id length */
+    size_t exts_off; /* to offset of extensions */
+    size_t ech_off; /* offset of ECH */
+    size_t sni_off; /* offset of (outer) SNI */
+    int echtype; /* ext type of the ECH */
+    int inner; /* 1 if the ECH is marked as an inner, 0 for outer */
+    /*
+     * A pointer to, and copy of, the hrrsignal from an HRR message.
+     * We need both, as we zero-out the octets when re-calculating and
+     * may need to put back what the server included so the transcript
+     * is correct when ECH acceptance failed.
+     */
+    unsigned char *hrrsignal_p;
+    unsigned char hrrsignal[OSSL_ECH_SIGNAL_LEN];
+    /*
+     * Fields that differ on client between inner and outer that we need to
+     * keep and swap over IFF ECH has succeeded. Same names chosen as are
+     * used in SSL_CONNECTION
+     */
+    EVP_PKEY *ks_pkey[OPENSSL_CLIENT_MAX_KEY_SHARES];
+    /* The IDs of the keyshare keys */
+    uint16_t ks_group_id[OPENSSL_CLIENT_MAX_KEY_SHARES];
+    size_t num_ks_pkey; /* how many keyshares are there */
+    unsigned char client_random[SSL3_RANDOM_SIZE]; /* CH random */
+} OSSL_ECH_CONN;
+
+/* Return values from ossl_ech_same_ext */
+#  define OSSL_ECH_SAME_EXT_ERR 0 /* bummer something wrong */
+#  define OSSL_ECH_SAME_EXT_DONE 1 /* proceed with same value in inner/outer */
+#  define OSSL_ECH_SAME_EXT_CONTINUE 2 /* generate a new value for outer CH */
+
+/*
+ * During extension construction (in extensions_clnt.c, and surprisingly also in
+ * extensions.c), we need to handle inner/outer CH cloning - ossl_ech_same_ext
+ * will (depending on compile time handling options) copy the value from
+ * CH.inner to CH.outer or else processing will continue, for a 2nd call,
+ * likely generating a fresh value for the outer CH. The fresh value could well
+ * be the same as in the inner.
+ *
+ * This macro should be called in each _ctos_ function that doesn't explicitly
+ * have special ECH handling. There are some _ctos_ functions that are called
+ * from a server, but we don't want to do anything in such cases. We also
+ * screen out cases where the context is not handling the ClientHello.
+ *
+ * Note that the placement of this macro needs a bit of thought - it has to go
+ * after declarations (to keep the ansi-c compile happy) and also after any
+ * checks that result in the extension not being sent but before any relevant
+ * state changes that would affect a possible 2nd call to the constructor.
+ * Luckily, that's usually not too hard, but it's not mechanical.
+ */
+#  define ECH_SAME_EXT(s, context, pkt) \
+    if (context == SSL_EXT_CLIENT_HELLO && !s->server \
+        && s->ext.ech.es != NULL && s->ext.ech.grease == 0) { \
+        int ech_iosame_rv = ossl_ech_same_ext(s, pkt); \
+        \
+        if (ech_iosame_rv == OSSL_ECH_SAME_EXT_ERR) \
+            return EXT_RETURN_FAIL; \
+        if (ech_iosame_rv == OSSL_ECH_SAME_EXT_DONE) \
+            return EXT_RETURN_SENT; \
+        /* otherwise continue as normal */ \
+    }
+
+/* Internal ECH APIs */
+
+OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old);
+void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee);
+void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce);
+int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx,
+                       const SSL_METHOD *method);
+void ossl_ech_conn_clear(OSSL_ECH_CONN *ec);
+void ossl_echext_free(OSSL_ECHEXT *e);
+OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src);
+#  ifdef OSSL_ECH_SUPERVERBOSE
+void ossl_ech_pbuf(const char *msg,
+                   const unsigned char *buf, const size_t blen);
+#  endif
+int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs,
+                               size_t *rcfgslen);
+int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt);
+int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee,
+                               OSSL_HPKE_SUITE *suite);
+int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded,
+                          size_t *encoded_len);
+int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr,
+                          unsigned char acbuf[OSSL_ECH_SIGNAL_LEN]);
+int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf,
+                             size_t blen);
+int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt);
+int ossl_ech_swaperoo(SSL_CONNECTION *s);
+int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
+                          unsigned char acbuf[OSSL_ECH_SIGNAL_LEN],
+                          const size_t shlen);
+
+/* these are internal but located in ssl/statem/extensions.c */
+int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt);
+int ossl_ech_same_key_share(void);
+int ossl_ech_2bcompressed(size_t ind);
+int ossl_ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, int ind,
+                              WPACKET *pkt);
+
+int ossl_ech_get_ch_offsets(SSL_CONNECTION *s, PACKET *pkt, size_t *sessid,
+                            size_t *exts, size_t *echoffset, uint16_t *echtype,
+                            int *inner, size_t *snioffset);
+int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt);
+void ossl_ech_status_print(BIO *out, SSL_CONNECTION *s, int selector);
+
+int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf,
+                        size_t blen, int hash_existing);
+int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen);
+size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
+                             size_t encoded_len);
+int ossl_ech_stash_keyshares(SSL_CONNECTION *s);
+int ossl_ech_unstash_keyshares(SSL_CONNECTION *s);
+
+# endif
+#endif
diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c
new file mode 100644
index 0000000000000..45e1ead616454
--- /dev/null
+++ b/ssl/ech/ech_ssl_apis.c
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include "internal/ssl_unwrap.h"
+#include "../ssl_local.h"
+
+int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es)
+{
+    if (ctx == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    OSSL_ECHSTORE_free(ctx->ext.ech.es);
+    ctx->ext.ech.es = NULL;
+    if (es == NULL)
+        return 1;
+    if ((ctx->ext.ech.es = ossl_echstore_dup(es)) == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    return 1;
+}
+
+int SSL_set1_echstore(SSL *ssl, OSSL_ECHSTORE *es)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return 0;
+    OSSL_ECHSTORE_free(s->ext.ech.es);
+    s->ext.ech.es = NULL;
+    if (es == NULL)
+        return 1;
+    if ((s->ext.ech.es = ossl_echstore_dup(es)) == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    /*
+     * Here, and below, if the application calls an API that implies it
+     * wants to try ECH, then we set attempted to 1
+     */
+    s->ext.ech.attempted = 1;
+    return 1;
+}
+
+OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx)
+{
+    OSSL_ECHSTORE *dup = NULL;
+
+    if (ctx == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return NULL;
+    }
+    if (ctx->ext.ech.es == NULL)
+        return NULL;
+    if ((dup = ossl_echstore_dup(ctx->ext.ech.es)) == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return NULL;
+    }
+    return dup;
+}
+
+OSSL_ECHSTORE *SSL_get1_echstore(const SSL *ssl)
+{
+    SSL_CONNECTION *s;
+    OSSL_ECHSTORE *dup = NULL;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return NULL;
+    }
+    if (s->ext.ech.es == NULL)
+        return NULL;
+    if ((dup = ossl_echstore_dup(s->ext.ech.es)) == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return NULL;
+    }
+    return dup;
+}
+
+int SSL_ech_set1_server_names(SSL *ssl, const char *inner_name,
+                              const char *outer_name, int no_outer)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return 0;
+    OPENSSL_free(s->ext.hostname);
+    s->ext.hostname = NULL;
+    if (inner_name != NULL) {
+        s->ext.hostname = OPENSSL_strdup(inner_name);
+        if (s->ext.hostname == NULL)
+            return 0;
+    }
+    OPENSSL_free(s->ext.ech.outer_hostname);
+    s->ext.ech.outer_hostname = NULL;
+    if (no_outer == 0 && outer_name != NULL && strlen(outer_name) > 0) {
+        s->ext.ech.outer_hostname = OPENSSL_strdup(outer_name);
+        if (s->ext.ech.outer_hostname == NULL)
+            return 0;
+    }
+    s->ext.ech.no_outer = no_outer;
+    s->ext.ech.attempted = 1;
+    return 1;
+}
+
+int SSL_ech_set1_outer_server_name(SSL *ssl, const char *outer_name,
+                                   int no_outer)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return 0;
+    OPENSSL_free(s->ext.ech.outer_hostname);
+    s->ext.ech.outer_hostname = NULL;
+    if (no_outer == 0 && outer_name != NULL && strlen(outer_name) > 0) {
+        s->ext.ech.outer_hostname = OPENSSL_strdup(outer_name);
+        if (s->ext.ech.outer_hostname == NULL)
+            return 0;
+    }
+    s->ext.ech.no_outer = no_outer;
+    s->ext.ech.attempted = 1;
+    return 1;
+}
+
+/*
+ * Note that this function returns 1 for success and 0 for error. This
+ * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL)
+ * returns 0 for success and 1 on error.
+ */
+int SSL_ech_set1_outer_alpn_protos(SSL *ssl, const unsigned char *protos,
+                                   const size_t protos_len)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return 0;
+    OPENSSL_free(s->ext.ech.alpn_outer);
+    s->ext.ech.alpn_outer = NULL;
+    if (protos == NULL)
+        return 1;
+    if (protos_len == 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    s->ext.ech.alpn_outer = OPENSSL_memdup(protos, protos_len);
+    if (s->ext.ech.alpn_outer == NULL)
+        return 0;
+    s->ext.ech.alpn_outer_len = protos_len;
+    s->ext.ech.attempted = 1;
+    return 1;
+}
+
+int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni)
+{
+    char *sinner = NULL;
+    char *souter = NULL;
+    SSL_CONNECTION *s = SSL_CONNECTION_FROM_SSL(ssl);
+
+    if (s == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return SSL_ECH_STATUS_FAILED;
+    }
+    if (outer_sni == NULL || inner_sni == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return SSL_ECH_STATUS_FAILED;
+    }
+    *outer_sni = NULL;
+    *inner_sni = NULL;
+    if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) {
+        if (s->ext.ech.returned != NULL)
+            return SSL_ECH_STATUS_GREASE_ECH;
+        return SSL_ECH_STATUS_GREASE;
+    }
+    if ((s->options & SSL_OP_ECH_GREASE) != 0 && s->ext.ech.attempted != 1)
+        return SSL_ECH_STATUS_GREASE;
+    if (s->ext.ech.backend == 1) {
+        if (s->ext.hostname != NULL
+            && (*inner_sni = OPENSSL_strdup(s->ext.hostname)) == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return SSL_ECH_STATUS_FAILED;
+        }
+        return SSL_ECH_STATUS_BACKEND;
+    }
+    if (s->ext.ech.es == NULL)
+        return SSL_ECH_STATUS_NOT_CONFIGURED;
+    /* Set output vars - note we may be pointing to NULL which is fine */
+    if (s->server == 0) {
+        sinner = s->ext.hostname;
+        if (s->ext.ech.attempted == 1 && s->ext.ech.success == 0)
+            sinner = s->ext.ech.former_inner;
+        if (s->ext.ech.no_outer == 0)
+            souter = s->ext.ech.outer_hostname;
+        else
+            souter = NULL;
+    } else {
+        if (s->ext.ech.es != NULL && s->ext.ech.success == 1) {
+            sinner = s->ext.hostname;
+            souter = s->ext.ech.outer_hostname;
+        }
+    }
+    if (s->ext.ech.es != NULL && s->ext.ech.attempted == 1
+        && s->ext.ech.attempted_type == TLSEXT_TYPE_ech
+        && s->ext.ech.grease != OSSL_ECH_IS_GREASE) {
+        long vr = X509_V_OK;
+
+        vr = SSL_get_verify_result(ssl);
+        if (sinner != NULL
+            && (*inner_sni = OPENSSL_strdup(sinner)) == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return SSL_ECH_STATUS_FAILED;
+        }
+        if (souter != NULL
+            && (*outer_sni = OPENSSL_strdup(souter)) == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return SSL_ECH_STATUS_FAILED;
+        }
+        if (s->ext.ech.success == 1) {
+            if (vr == X509_V_OK)
+                return SSL_ECH_STATUS_SUCCESS;
+            else
+                return SSL_ECH_STATUS_BAD_NAME;
+        } else {
+            if (vr == X509_V_OK && s->ext.ech.returned != NULL)
+                return SSL_ECH_STATUS_FAILED_ECH;
+            else if (vr != X509_V_OK && s->ext.ech.returned != NULL)
+                return SSL_ECH_STATUS_FAILED_ECH_BAD_NAME;
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return SSL_ECH_STATUS_FAILED;
+        }
+    }
+    return SSL_ECH_STATUS_NOT_TRIED;
+}
+
+int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return 0;
+    OPENSSL_free(s->ext.ech.grease_suite);
+    s->ext.ech.grease_suite = NULL;
+    if (suite == NULL)
+        return 1;
+    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;
+}
+
+int SSL_ech_set_grease_type(SSL *ssl, uint16_t type)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    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;
+}
+
+void SSL_ech_set_callback(SSL *ssl, SSL_ech_cb_func f)
+{
+    SSL_CONNECTION *s;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL)
+        return;
+    s->ext.ech.cb = f;
+    return;
+}
+
+int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen)
+{
+    SSL_CONNECTION *s;
+    OSSL_ECHSTORE *ve = NULL;
+    BIO *in = NULL;
+    int rv = 0;
+    OSSL_LIB_CTX *libctx = NULL;
+    const char *propq = NULL;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL || ec == NULL || eclen == NULL
+        || s->ext.ech.returned_len > INT_MAX) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        goto err;
+    }
+    if (s->ext.ech.returned == NULL) {
+        *ec = NULL;
+        *eclen = 0;
+        return 1;
+    }
+    /*
+     * To not hand rubbish to application, we'll decode the value we have
+     * so only syntactically good things are passed up. We won't insist
+     * though that every entry in the retry_config list seems good - it
+     * could be that e.g. one is a newer version than we support now,
+     * and letting the application see that might cause someone to do an
+     * upgrade.
+     */
+    if (s->ext.ech.es != NULL) {
+        libctx = s->ext.ech.es->libctx;
+        propq = s->ext.ech.es->propq;
+    }
+    if ((in = BIO_new(BIO_s_mem())) == NULL
+        || BIO_write(in, s->ext.ech.returned, (int)s->ext.ech.returned_len) <= 0
+        || (ve = OSSL_ECHSTORE_new(libctx, propq)) == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (OSSL_ECHSTORE_read_echconfiglist(ve, in) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* all good, copy and return */
+    *ec = OPENSSL_memdup(s->ext.ech.returned, s->ext.ech.returned_len);
+    if (*ec == NULL)
+        goto err;
+    *eclen = s->ext.ech.returned_len;
+    rv = 1;
+err:
+    OSSL_ECHSTORE_free(ve);
+    BIO_free_all(in);
+    return rv;
+}
+
+/*
+ * Note that this function returns 1 for success and 0 for error. This
+ * contrasts with SSL_CTX_set1_alpn_protos() which (unusually for OpenSSL)
+ * returns 0 for success and 1 on error.
+ */
+int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx,
+                                       const unsigned char *protos,
+                                       const size_t protos_len)
+{
+    if (ctx == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    OPENSSL_free(ctx->ext.ech.alpn_outer);
+    ctx->ext.ech.alpn_outer = NULL;
+    if (protos == NULL)
+        return 1;
+    if (protos_len == 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    ctx->ext.ech.alpn_outer = OPENSSL_memdup(protos, protos_len);
+    if (ctx->ext.ech.alpn_outer == NULL)
+        return 0;
+    ctx->ext.ech.alpn_outer_len = protos_len;
+    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) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return;
+    }
+    ctx->ext.ech.cb = f;
+    return;
+}
+
+int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len)
+{
+    int rv = 0;
+    SSL_CONNECTION *s;
+    OSSL_ECHSTORE *es = NULL;
+    BIO *es_in = NULL;
+
+    s = SSL_CONNECTION_FROM_SSL(ssl);
+    if (s == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        goto err;
+    }
+    if (ecl == NULL) {
+        OSSL_ECHSTORE_free(s->ext.ech.es);
+        s->ext.ech.es = NULL;
+        return 1;
+    }
+    if (ecl_len == 0 || ecl_len > INT_MAX) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        goto err;
+    }
+    if ((es_in = BIO_new_mem_buf(ecl, (int)ecl_len)) == NULL
+        || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL
+        || OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1
+        || SSL_set1_echstore(ssl, es) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    rv = 1;
+err:
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(es_in);
+    return rv;
+}
diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c
new file mode 100644
index 0000000000000..15c559e144f96
--- /dev/null
+++ b/ssl/ech/ech_store.c
@@ -0,0 +1,1141 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the OpenSSL license (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include "../ssl_local.h"
+#include "ech_local.h"
+#include 
+#include 
+#include 
+
+/* a size for some crypto vars */
+#define OSSL_ECH_CRYPTO_VAR_SIZE 2048
+
+/*
+ * Used for ech_bio2buf, when reading from a BIO we allocate in chunks sized
+ * as per below, with a max number of chunks as indicated, we don't expect to
+ * go beyond one chunk in almost all cases
+ */
+#define OSSL_ECH_BUFCHUNK 512
+#define OSSL_ECH_MAXITER  32
+
+/*
+ * ECHConfigList input to OSSL_ECHSTORE_read_echconfiglist()
+ * can be either binary encoded ECHConfigList or a base64
+ * encoded ECHConfigList.
+ */
+#define OSSL_ECH_FMT_BIN       1  /* binary ECHConfigList */
+#define OSSL_ECH_FMT_B64TXT    2  /* base64 ECHConfigList */
+
+/*
+ * Telltales we use when guessing which form of encoded input we've
+ * been given for an RR value or ECHConfig.
+ * We give these the EBCDIC treatment as well - why not? :-)
+ */
+static const char B64_alphabet[] =
+    "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52"
+    "\x53\x54\x55\x56\x57\x58\x59\x5a\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a"
+    "\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x30\x31"
+    "\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d\x3b";
+
+#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?
+ */
+# define TLSEXT_MINLEN_host_name 4
+#endif
+
+/*
+ * local functions - public APIs are at the end
+ */
+
+void ossl_echext_free(OSSL_ECHEXT *e)
+{
+    if (e == NULL)
+        return;
+    OPENSSL_free(e->val);
+    OPENSSL_free(e);
+    return;
+}
+
+OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src)
+{
+    OSSL_ECHEXT *ext = OPENSSL_zalloc(sizeof(*src));
+
+    if (ext == NULL)
+        return NULL;
+    *ext = *src;
+    ext->val = NULL;
+    if (ext->len != 0) {
+        ext->val = OPENSSL_memdup(src->val, src->len);
+        if (ext->val == NULL) {
+            ossl_echext_free(ext);
+            return NULL;
+        }
+    }
+    return ext;
+}
+
+void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee)
+{
+    if (ee == NULL)
+        return;
+    OPENSSL_free(ee->public_name);
+    OPENSSL_free(ee->pub);
+    EVP_PKEY_free(ee->keyshare);
+    OPENSSL_free(ee->encoded);
+    OPENSSL_free(ee->suites);
+    sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free);
+    OPENSSL_free(ee);
+    return;
+}
+
+/*
+ * @brief Read a buffer from an input 'till eof
+ * @param in is the BIO input
+ * @param buf is where to put the buffer, allocated inside here
+ * @param len is the length of that buffer
+ *
+ * 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?
+ */
+static int ech_bio2buf(BIO *in, unsigned char **buf, size_t *len)
+{
+    unsigned char *lptr = NULL, *lbuf = NULL, *tmp = NULL;
+    size_t sofar = 0, readbytes = 0;
+    int done = 0, brv, iter = 0;
+
+    if (buf == NULL || len == NULL)
+        return 0;
+    sofar = OSSL_ECH_BUFCHUNK;
+    lbuf = OPENSSL_zalloc(sofar);
+    if (lbuf == NULL)
+        return 0;
+    lptr = lbuf;
+    while (!BIO_eof(in) && !done && iter++ < OSSL_ECH_MAXITER) {
+        brv = BIO_read_ex(in, lptr, OSSL_ECH_BUFCHUNK, &readbytes);
+        if (brv != 1)
+            goto err;
+        if (readbytes < OSSL_ECH_BUFCHUNK) {
+            done = 1;
+            break;
+        }
+        sofar += OSSL_ECH_BUFCHUNK;
+        tmp = OPENSSL_realloc(lbuf, sofar);
+        if (tmp == NULL)
+            goto err;
+        lbuf = tmp;
+        lptr = lbuf + sofar - OSSL_ECH_BUFCHUNK;
+    }
+    if (BIO_eof(in) && done == 1) {
+        *len = sofar + readbytes - OSSL_ECH_BUFCHUNK;
+        *buf = lbuf;
+        return 1;
+    }
+err:
+    OPENSSL_free(lbuf);
+    return 0;
+}
+
+/*
+ * @brief Figure out ECHConfig encoding
+ * @param encodedval is a buffer with the encoding
+ * @param encodedlen is the length of that buffer
+ * @param guessedfmt is the detected format
+ * @return 1 for success, 0 for error
+ */
+static int ech_check_format(const unsigned char *val, size_t len, int *fmt)
+{
+    size_t span = 0;
+
+    if (fmt == NULL || len <= 4 || val == NULL)
+        return 0;
+    /* binary encoding starts with two octet length and ECH version */
+    if (len == 2 + ((size_t)(val[0]) * 256 + (size_t)(val[1]))
+        && val[2] == ((OSSL_ECH_RFCXXXX_VERSION / 256) & 0xff)
+        && val[3] == ((OSSL_ECH_RFCXXXX_VERSION % 256) & 0xff)) {
+        *fmt = OSSL_ECH_FMT_BIN;
+        return 1;
+    }
+    span = strspn((char *)val, B64_alphabet);
+    if (len <= span) {
+        *fmt = OSSL_ECH_FMT_B64TXT;
+        return 1;
+    }
+    return 0;
+}
+
+/*
+ * @brief helper to decode ECHConfig extensions
+ * @param ee is the OSSL_ECHSTORE entry for these
+ * @param exts is the binary form extensions
+ * @return 1 for good, 0 for error
+ */
+static int ech_decode_echconfig_exts(OSSL_ECHSTORE_ENTRY *ee, PACKET *exts)
+{
+    unsigned int exttype = 0;
+    size_t extlen = 0;
+    unsigned char *extval = NULL;
+    OSSL_ECHEXT *oe = NULL;
+    PACKET ext;
+
+    /*
+     * reminder: exts is a two-octet length prefixed list of:
+     * - two octet extension type
+     * - two octet extension length (can be zero)
+     * - length octets
+     * we've consumed the overall length before getting here
+     */
+    while (PACKET_remaining(exts) > 0) {
+        exttype = 0, extlen = 0;
+        extval = NULL;
+        oe = NULL;
+        if (!PACKET_get_net_2(exts, &exttype) ||
+            !PACKET_get_length_prefixed_2(exts, &ext)) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
+            goto err;
+        }
+        if (PACKET_remaining(&ext) >= OSSL_ECH_MAX_ECHCONFIGEXT_LEN) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
+            goto err;
+        }
+        if (!PACKET_memdup(&ext, &extval, &extlen)) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
+            goto err;
+        }
+        oe = OPENSSL_malloc(sizeof(*oe));
+        if (oe == NULL)
+            goto err;
+        oe->type = (uint16_t) exttype;
+        oe->val = extval;
+        extval = NULL; /* avoid double free */
+        oe->len = (uint16_t) extlen;
+        if (ee->exts == NULL)
+            ee->exts = sk_OSSL_ECHEXT_new_null();
+        if (ee->exts == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        if (!sk_OSSL_ECHEXT_push(ee->exts, oe)) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    }
+    return 1;
+err:
+    sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free);
+    ee->exts = NULL;
+    ossl_echext_free(oe);
+    OPENSSL_free(extval);
+    return 0;
+}
+
+/*
+ * @brief Check entry to see if looks good or bad
+ * @param ee is the ECHConfig to check
+ * @return 1 for all good, 0 otherwise
+ */
+static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee)
+{
+    OSSL_HPKE_SUITE hpke_suite;
+    int ind, num;
+    int goodsuitefound = 0;
+
+    /* check local support for some suite */
+    for (ind = 0; ind != (int)ee->nsuites; ind++) {
+        /*
+         * suite_check says yes to the pseudo-aead for export, but we don't
+         * want to see it here coming from outside in an encoding
+         */
+        hpke_suite = ee->suites[ind];
+        if (OSSL_HPKE_suite_check(hpke_suite) == 1
+            && hpke_suite.aead_id != OSSL_HPKE_AEAD_ID_EXPORTONLY) {
+            goodsuitefound = 1;
+            break;
+        }
+    }
+    if (goodsuitefound == 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    /* check no mandatory exts (with high bit set in type) */
+    num = (ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
+    for (ind = 0; ind != num; ind++) {
+        OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, (int)ind);
+
+        if (oe->type & 0x8000) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+            return 0;
+        }
+    }
+    /* check public_name rules, as per spec section 4 */
+    if (ee->public_name == NULL
+        || ee->public_name[0] == '\0'
+        || ee->public_name[0] == '.'
+        || ee->public_name[strlen(ee->public_name) - 1] == '.')
+        return 0;
+    return 1;
+}
+
+/**
+ * @brief decode one ECHConfig from a packet into an entry
+ * @param rent ptr to an entry allocated within (on success)
+ * @param pkt is the encoding
+ * @param priv is an optional private key (NULL if absent)
+ * @param for_retry says whether to include in a retry_config (if priv present)
+ * @return 1 for success, 0 for error
+ */
+static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt,
+                                EVP_PKEY *priv, int for_retry)
+{
+    size_t ech_content_length = 0;
+    unsigned int tmpi;
+    const unsigned char *tmpecp = NULL;
+    size_t tmpeclen = 0, test_publen = 0;
+    PACKET ver_pkt, pub_pkt, cipher_suites, public_name_pkt, exts;
+    uint16_t thiskemid;
+    size_t suiteoctets = 0;
+    unsigned int ci = 0;
+    unsigned char cipher[OSSL_ECH_CIPHER_LEN], max_name_len;
+    unsigned char test_pub[OSSL_ECH_CRYPTO_VAR_SIZE];
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    if (rent == NULL || pkt == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    ee = OPENSSL_zalloc(sizeof(*ee));
+    if (ee == NULL)
+        goto err;
+    /* note start of encoding so we can make a copy later */
+    tmpeclen = PACKET_remaining(pkt);
+    if (PACKET_peek_bytes(pkt, &tmpecp, tmpeclen) != 1
+        || !PACKET_get_net_2(pkt, &tmpi)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    ee->version = (uint16_t) tmpi;
+
+    /* grab versioned packet data */
+    if (!PACKET_get_length_prefixed_2(pkt, &ver_pkt)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    ech_content_length = (unsigned int)PACKET_remaining(&ver_pkt);
+    switch (ee->version) {
+    case OSSL_ECH_RFCXXXX_VERSION:
+        break;
+    default:
+        /* skip over in case we get something we can handle later */
+        if (!PACKET_forward(&ver_pkt, ech_content_length)) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+            goto err;
+        }
+        /* nothing to return but not a fail */
+        ossl_echstore_entry_free(ee);
+        *rent = NULL;
+        return 1;
+    }
+    if (!PACKET_copy_bytes(&ver_pkt, &ee->config_id, 1)
+        || !PACKET_get_net_2(&ver_pkt, &tmpi)
+        || !PACKET_get_length_prefixed_2(&ver_pkt, &pub_pkt)
+        || !PACKET_memdup(&pub_pkt, &ee->pub, &ee->pub_len)
+        || !PACKET_get_length_prefixed_2(&ver_pkt, &cipher_suites)
+        || (suiteoctets = PACKET_remaining(&cipher_suites)) <= 0
+        || (suiteoctets % 2) == 1
+        || suiteoctets / OSSL_ECH_CIPHER_LEN > UINT_MAX) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    thiskemid = (uint16_t) tmpi;
+    ee->nsuites = (unsigned int)(suiteoctets / OSSL_ECH_CIPHER_LEN);
+    ee->suites = OPENSSL_malloc(ee->nsuites * sizeof(*ee->suites));
+    if (ee->suites == NULL)
+        goto err;
+    while (PACKET_copy_bytes(&cipher_suites, cipher,
+                             OSSL_ECH_CIPHER_LEN)) {
+        ee->suites[ci].kem_id = thiskemid;
+        ee->suites[ci].kdf_id = cipher[0] << 8 | cipher [1];
+        ee->suites[ci].aead_id = cipher[2] << 8 | cipher [3];
+        if (ci++ >= ee->nsuites) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+            goto err;
+        }
+    }
+    if (PACKET_remaining(&cipher_suites) > 0
+        || !PACKET_copy_bytes(&ver_pkt, &max_name_len, 1)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    ee->max_name_length = max_name_len;
+    if (!PACKET_get_length_prefixed_1(&ver_pkt, &public_name_pkt)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    if (PACKET_contains_zero_byte(&public_name_pkt)
+        || PACKET_remaining(&public_name_pkt) < TLSEXT_MINLEN_host_name
+        || !PACKET_strndup(&public_name_pkt, &ee->public_name)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    if (!PACKET_get_length_prefixed_2(&ver_pkt, &exts)) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    if (PACKET_remaining(&exts) > 0
+        && ech_decode_echconfig_exts(ee, &exts) != 1) {
+        ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+        goto err;
+    }
+    /* set length of encoding of this ECHConfig */
+    ee->encoded_len = PACKET_data(&ver_pkt) - tmpecp;
+    /* copy encoded as it might get free'd if a reduce happens */
+    ee->encoded = OPENSSL_memdup(tmpecp, ee->encoded_len);
+    if (ee->encoded == NULL)
+        goto err;
+    if (priv != NULL) {
+        if (EVP_PKEY_get_octet_string_param(priv,
+                                            OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY,
+                                            test_pub, OSSL_ECH_CRYPTO_VAR_SIZE,
+                                            &test_publen) != 1) {
+            ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
+            goto err;
+        }
+        if (test_publen == ee->pub_len
+            && !memcmp(test_pub, ee->pub, ee->pub_len)) {
+            EVP_PKEY_up_ref(priv); /* associate the private key */
+            ee->keyshare = priv;
+            ee->for_retry = for_retry;
+        }
+    }
+    ee->loadtime = time(0);
+    *rent = ee;
+    return 1;
+err:
+    ossl_echstore_entry_free(ee);
+    *rent = NULL;
+    return 0;
+}
+
+/*
+ * @brief decode and flatten a binary encoded ECHConfigList
+ * @param es an OSSL_ECHSTORE
+ * @param priv is an optional private key (NULL if absent)
+ * @param for_retry says whether to include in a retry_config (if priv present)
+ * @param binbuf binary encoded ECHConfigList (we hope)
+ * @param binlen length of binbuf
+ * @return 1 for success, 0 for error
+ *
+ * We may only get one ECHConfig per list, but there can be more.  We want each
+ * element of the output to contain exactly one ECHConfig so that a client
+ * could sensibly down select to the one they prefer later, and so that we have
+ * the specific encoded value of that ECHConfig for inclusion in the HPKE info
+ * parameter when finally encrypting or decrypting an inner ClientHello.
+ *
+ * If a private value is provided then that'll only be associated with the
+ * relevant public value, if >1 public value was present in the ECHConfigList.
+ */
+static int ech_decode_and_flatten(OSSL_ECHSTORE *es, EVP_PKEY *priv, int for_retry,
+                                  unsigned char *binbuf, size_t binblen)
+{
+    int rv = 0;
+    size_t remaining = 0;
+    PACKET opkt, pkt;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    if (binbuf == NULL || binblen == 0 || binblen < OSSL_ECH_MIN_ECHCONFIG_LEN
+        || binblen >= OSSL_ECH_MAX_ECHCONFIG_LEN) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        goto err;
+    }
+    if (PACKET_buf_init(&opkt, binbuf, binblen) != 1
+        || !PACKET_get_length_prefixed_2(&opkt, &pkt)) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    remaining = PACKET_remaining(&pkt);
+    while (remaining > 0) {
+        if (ech_decode_one_entry(&ee, &pkt, priv, for_retry) != 1) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        remaining = PACKET_remaining(&pkt);
+        /* if unsupported version we can skip over */
+        if (ee == NULL)
+            continue;
+        /* do final checks on suites, exts, and fail if issues */
+        if (ech_final_config_checks(ee) != 1)
+            goto err;
+        /* push entry into store */
+        if (es->entries == NULL)
+            es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null();
+        if (es->entries == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        ee = NULL;
+    }
+    rv = 1;
+err:
+    ossl_echstore_entry_free(ee);
+    return rv;
+}
+
+/*
+ * @brief check a private matches some public
+ * @param es is the ECH store
+ * @param priv is the private value
+ * @return 1 if we have a match, zero otherwise
+ */
+static int check_priv_matches(OSSL_ECHSTORE *es, EVP_PKEY *priv)
+{
+    int num, ent, gotone = 0;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    for (ent = 0; ent != num; ent++) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, ent);
+        if (ee == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+            return 0;
+        }
+        if (EVP_PKEY_eq(ee->keyshare, priv)) {
+            gotone = 1;
+            break;
+        }
+    }
+    return gotone;
+}
+
+/*
+ * @brief decode input ECHConfigList and associate optional private info
+ * @param es is the OSSL_ECHSTORE
+ * @param in is the BIO from which we'll get the ECHConfigList
+ * @param priv is an optional private key
+ * @param for_retry 1 if the public related to priv ought be in retry_config
+ */
+static int ech_read_priv_echconfiglist(OSSL_ECHSTORE *es, BIO *in,
+                                       EVP_PKEY *priv, int for_retry)
+{
+    int rv = 0, detfmt, tdeclen = 0;
+    size_t encodedlen = 0, binlen = 0;
+    unsigned char *encodedval = NULL, *binbuf = NULL;
+    BIO *btmp = NULL, *btmp1 = NULL;
+
+    if (es == NULL || in == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    if (ech_bio2buf(in, &encodedval, &encodedlen) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if (encodedlen >= OSSL_ECH_MAX_ECHCONFIG_LEN) { /* sanity check */
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (ech_check_format(encodedval, encodedlen, &detfmt) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        goto err;
+    }
+    if (detfmt == OSSL_ECH_FMT_BIN) { /* copy buffer if binary format */
+        binbuf = OPENSSL_memdup(encodedval, encodedlen);
+        if (binbuf == NULL)
+            goto err;
+        binlen = encodedlen;
+    }
+    if (detfmt == OSSL_ECH_FMT_B64TXT) {
+        btmp = BIO_new_mem_buf(encodedval, encodedlen);
+        if (btmp == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        btmp1 = BIO_new(BIO_f_base64());
+        if (btmp1 == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        BIO_set_flags(btmp1, BIO_FLAGS_BASE64_NO_NL);
+        btmp = BIO_push(btmp1, btmp);
+        /* overestimate but good enough */
+        binbuf = OPENSSL_malloc(encodedlen);
+        if (binbuf == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        tdeclen = BIO_read(btmp, binbuf, (int)encodedlen);
+        if (tdeclen <= 0) { /* need int for -1 return in failure case */
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        binlen = tdeclen;
+    }
+    if (ech_decode_and_flatten(es, priv, for_retry, binbuf, binlen) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (priv != NULL && check_priv_matches(es, priv) == 0)
+        goto err;
+    rv = 1;
+err:
+    BIO_free_all(btmp);
+    OPENSSL_free(binbuf);
+    OPENSSL_free(encodedval);
+    return rv;
+}
+
+/*
+ * API calls built around OSSL_ECHSSTORE
+ */
+
+OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq)
+{
+    OSSL_ECHSTORE *es = NULL;
+
+    es = OPENSSL_zalloc(sizeof(*es));
+    if (es == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    es->libctx = libctx;
+    if (propq != NULL) {
+        es->propq = OPENSSL_strdup(propq);
+        if (es->propq == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+    }
+
+    return es;
+}
+
+void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es)
+{
+    if (es == NULL)
+        return;
+    sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free);
+    OPENSSL_free(es->propq);
+    OPENSSL_free(es);
+    return;
+}
+
+int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
+                             uint16_t echversion, uint8_t max_name_length,
+                             const char *public_name, OSSL_HPKE_SUITE suite)
+{
+    size_t pnlen = 0, publen = OSSL_ECH_CRYPTO_VAR_SIZE;
+    unsigned char pub[OSSL_ECH_CRYPTO_VAR_SIZE];
+    int rv = 0;
+    unsigned char *bp = NULL;
+    size_t bblen = 0;
+    EVP_PKEY *privp = NULL;
+    uint8_t config_id = 0;
+    WPACKET epkt;
+    BUF_MEM *epkt_mem = NULL;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    /* basic checks */
+    if (es == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    pnlen = (public_name == NULL ? 0 : strlen(public_name));
+    if (pnlen == 0 || pnlen > OSSL_ECH_MAX_PUBLICNAME
+        || max_name_length > OSSL_ECH_MAX_MAXNAMELEN) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    /* this used have more versions and will again in future */
+    switch (echversion) {
+    case OSSL_ECH_RFCXXXX_VERSION:
+        break;
+    default:
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    /*
+     *   Reminder, for draft-13 we want this:
+     *
+     *   opaque HpkePublicKey<1..2^16-1>;
+     *   uint16 HpkeKemId;  // Defined in I-D.irtf-cfrg-hpke
+     *   uint16 HpkeKdfId;  // Defined in I-D.irtf-cfrg-hpke
+     *   uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke
+     *   struct {
+     *       HpkeKdfId kdf_id;
+     *       HpkeAeadId aead_id;
+     *   } HpkeSymmetricCipherSuite;
+     *   struct {
+     *       uint8 config_id;
+     *       HpkeKemId kem_id;
+     *       HpkePublicKey public_key;
+     *       HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>;
+     *   } HpkeKeyConfig;
+     *   struct {
+     *       HpkeKeyConfig key_config;
+     *       uint8 maximum_name_length;
+     *       opaque public_name<1..255>;
+     *       Extension extensions<0..2^16-1>;
+     *   } ECHConfigContents;
+     *   struct {
+     *       uint16 version;
+     *       uint16 length;
+     *       select (ECHConfig.version) {
+     *         case 0xfe0d: ECHConfigContents contents;
+     *       }
+     *   } ECHConfig;
+     *   ECHConfig ECHConfigList<1..2^16-1>;
+     */
+    if ((epkt_mem = BUF_MEM_new()) == NULL
+        || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)
+        || !WPACKET_init(&epkt, epkt_mem)) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* random config_id */
+    if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, 0) <= 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* key pair */
+    if (OSSL_HPKE_keygen(suite, pub, &publen, &privp, NULL, 0,
+                         es->libctx, es->propq) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* config id, KEM, public, KDF, AEAD, max name len, public_name, exts */
+    if ((bp = WPACKET_get_curr(&epkt)) == NULL
+        || !WPACKET_start_sub_packet_u16(&epkt)
+        || !WPACKET_put_bytes_u16(&epkt, echversion)
+        || !WPACKET_start_sub_packet_u16(&epkt)
+        || !WPACKET_put_bytes_u8(&epkt, config_id)
+        || !WPACKET_put_bytes_u16(&epkt, suite.kem_id)
+        || !WPACKET_start_sub_packet_u16(&epkt)
+        || !WPACKET_memcpy(&epkt, pub, publen)
+        || !WPACKET_close(&epkt)
+        || !WPACKET_start_sub_packet_u16(&epkt)
+        || !WPACKET_put_bytes_u16(&epkt, suite.kdf_id)
+        || !WPACKET_put_bytes_u16(&epkt, suite.aead_id)
+        || !WPACKET_close(&epkt)
+        || !WPACKET_put_bytes_u8(&epkt, max_name_length)
+        || !WPACKET_start_sub_packet_u8(&epkt)
+        || !WPACKET_memcpy(&epkt, public_name, pnlen)
+        || !WPACKET_close(&epkt)
+        || !WPACKET_start_sub_packet_u16(&epkt)
+        || !WPACKET_memcpy(&epkt, NULL, 0) /* no extensions */
+        || !WPACKET_close(&epkt)
+        || !WPACKET_close(&epkt)
+        || !WPACKET_close(&epkt)) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* bp, bblen has encoding */
+    WPACKET_get_total_written(&epkt, &bblen);
+    if ((ee = OPENSSL_zalloc(sizeof(*ee))) == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    ee->suites = OPENSSL_malloc(sizeof(*ee->suites));
+    if (ee->suites == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    ee->version = echversion;
+    ee->pub_len = publen;
+    ee->pub = OPENSSL_memdup(pub, publen);
+    if (ee->pub == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    ee->nsuites = 1;
+    ee->suites[0] = suite;
+    ee->public_name = OPENSSL_strdup(public_name);
+    if (ee->public_name == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    ee->max_name_length = max_name_length;
+    ee->config_id = config_id;
+    ee->keyshare = privp;
+    /* "steal" the encoding from the memory */
+    ee->encoded = (unsigned char *)epkt_mem->data;
+    ee->encoded_len = bblen;
+    epkt_mem->data = NULL;
+    epkt_mem->length = 0;
+    ee->loadtime = time(0);
+    /* push entry into store */
+    if (es->entries == NULL)
+        es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null();
+    if (es->entries == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    WPACKET_finish(&epkt);
+    BUF_MEM_free(epkt_mem);
+    return 1;
+
+err:
+    EVP_PKEY_free(privp);
+    WPACKET_cleanup(&epkt);
+    BUF_MEM_free(epkt_mem);
+    ossl_echstore_entry_free(ee);
+    return rv;
+}
+
+int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out)
+{
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    int rv = 0, num = 0, chosen = 0, doall = 0;
+    WPACKET epkt; /* used if we want to merge ECHConfigs for output */
+    BUF_MEM *epkt_mem = NULL;
+    size_t allencoded_len;
+
+    if (es == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    if (num <= 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    if (index >= num) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    if (index == OSSL_ECHSTORE_ALL)
+        doall = 1;
+    else if (index == OSSL_ECHSTORE_LAST)
+        chosen = num - 1;
+    else
+        chosen = index;
+    memset(&epkt, 0, sizeof(epkt));
+    if (doall == 0) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen);
+        if (ee == NULL || ee->encoded == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+            return 0;
+        }
+        /* private key first */
+        if (ee->keyshare != NULL
+            && !PEM_write_bio_PrivateKey(out, ee->keyshare, NULL, NULL, 0,
+                                         NULL, NULL)) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL,
+                          ee->encoded, (long)ee->encoded_len) <= 0) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    } else {
+        /* catenate the encodings into one */
+        if ((epkt_mem = BUF_MEM_new()) == NULL
+            || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)
+            || !WPACKET_init(&epkt, epkt_mem)
+            || !WPACKET_start_sub_packet_u16(&epkt)) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        for (chosen = 0; chosen != num; chosen++) {
+            ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen);
+            if (ee == NULL || ee->encoded == NULL) {
+                ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+                return 0;
+            }
+            if (!WPACKET_memcpy(&epkt, ee->encoded, ee->encoded_len)) {
+                ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        }
+        if (!WPACKET_close(&epkt)) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        WPACKET_get_total_written(&epkt, &allencoded_len);
+        if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL,
+                          (unsigned char *)epkt_mem->data,
+                          (long)allencoded_len) <= 0) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    }
+    rv = 1;
+err:
+    WPACKET_cleanup(&epkt);
+    BUF_MEM_free(epkt_mem);
+    return rv;
+}
+
+int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in)
+{
+    return ech_read_priv_echconfiglist(es, in, NULL, 0);
+}
+
+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)
+{
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    unsigned int j = 0;
+    int num = 0;
+    BIO *out = NULL;
+    time_t now = time(0);
+    size_t ehlen;
+    unsigned char *ignore = NULL;
+
+    if (es == NULL || loaded_secs == NULL || public_name == NULL
+        || echconfig == NULL || has_private == NULL || for_retry == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    if (num == 0 || index < 0 || index >= num) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, index);
+    if (ee == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    *loaded_secs = now - ee->loadtime;
+    *public_name = NULL;
+    *echconfig = NULL;
+    if (ee->public_name != NULL) {
+        *public_name = OPENSSL_strdup(ee->public_name);
+        if (*public_name == NULL)
+            goto err;
+    }
+    *has_private = (ee->keyshare == NULL ? 0 : 1);
+    /* Now "print" the ECHConfigList */
+    out = BIO_new(BIO_s_mem());
+    if (out == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    if (ee->version != OSSL_ECH_RFCXXXX_VERSION) {
+        /* just note we don't support that one today */
+        BIO_printf(out, "[Unsupported version (%04x)]", ee->version);
+    } else {
+        /* version, config_id, public_name, and kem */
+        BIO_printf(out, "[%04x,%02x,%s,[", ee->version, ee->config_id,
+                   ee->public_name != NULL ? (char *)ee->public_name : "NULL");
+        /* ciphersuites */
+        for (j = 0; j != ee->nsuites; j++) {
+            BIO_printf(out, "%04x,%04x,%04x", ee->suites[j].kem_id,
+                       ee->suites[j].kdf_id, ee->suites[j].aead_id);
+            if (j < (ee->nsuites - 1))
+                BIO_printf(out, ",");
+        }
+        BIO_printf(out, "],");
+        /* public key */
+        for (j = 0; j != ee->pub_len; j++)
+            BIO_printf(out, "%02x", ee->pub[j]);
+        /* max name length and (only) number of extensions */
+        BIO_printf(out, ",%02x,%02x]", ee->max_name_length,
+                   ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
+    }
+    ehlen = BIO_get_mem_data(out, &ignore);
+    if (ehlen > INT_MAX)
+        goto err;
+    *echconfig = OPENSSL_malloc(ehlen + 1);
+    if (*echconfig == NULL)
+        goto err;
+    if (BIO_read(out, *echconfig, (int)ehlen) <= 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    (*echconfig)[ehlen] = '\0';
+    BIO_free(out);
+    return 1;
+err:
+    BIO_free(out);
+    OPENSSL_free(*public_name);
+    *public_name = NULL;
+    OPENSSL_free(*echconfig);
+    *echconfig = NULL;
+    return 0;
+}
+
+int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index)
+{
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    int i, num = 0, chosen = OSSL_ECHSTORE_ALL;
+
+    if (es == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    if (num == 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    if (index <= OSSL_ECHSTORE_ALL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    if (index == OSSL_ECHSTORE_LAST) {
+        chosen = num - 1;
+    } else if (index >= num) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    } else {
+        chosen = index;
+    }
+    for (i = num - 1; i >= 0; i--) {
+        if (i == chosen)
+            continue;
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
+        ossl_echstore_entry_free(ee);
+        sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i);
+    }
+    return 1;
+}
+
+int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv,
+                                        BIO *in, int for_retry)
+{
+    unsigned char *b64 = NULL;
+    long b64len = 0;
+    BIO *b64bio = NULL;
+    int rv = 0;
+    char *pname = NULL, *pheader = NULL;
+
+    /* we allow for a NULL private key */
+    if (es == NULL || in == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    if (PEM_read_bio(in, &pname, &pheader, &b64, &b64len) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if (pname == NULL || strcmp(pname, PEM_STRING_ECHCONFIG) != 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    b64bio = BIO_new(BIO_s_mem());
+    if (b64bio == NULL
+        || BIO_write(b64bio, b64, b64len) <= 0
+        || ech_read_priv_echconfiglist(es, b64bio, priv, for_retry) != 1) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    rv = 1;
+err:
+    OPENSSL_free(pname);
+    OPENSSL_free(pheader);
+    BIO_free_all(b64bio);
+    OPENSSL_free(b64);
+    return rv;
+}
+
+int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry)
+{
+    EVP_PKEY *priv = NULL;
+    int rv = 0;
+    BIO *fbio = BIO_new(BIO_f_buffer());
+
+    if (fbio == NULL || es == NULL || in == NULL) {
+        BIO_free_all(fbio);
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    /*
+     * Read private key then handoff to set1_key_and_read_pem.
+     * We allow for no private key as an option, to handle that
+     * the BIO_f_buffer allows us to seek back to the start.
+     */
+    BIO_push(fbio, in);
+    if (!PEM_read_bio_PrivateKey(fbio, &priv, NULL, NULL)
+        && BIO_seek(fbio, 0) < 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    rv = OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, fbio, for_retry);
+err:
+    EVP_PKEY_free(priv);
+    BIO_pop(fbio);
+    BIO_free_all(fbio);
+    return rv;
+}
+
+int OSSL_ECHSTORE_num_entries(const OSSL_ECHSTORE *es, int *numentries)
+{
+    if (es == NULL || numentries == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    *numentries = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    return 1;
+}
+
+int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys)
+{
+    int i, num = 0, count = 0;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+
+    if (es == NULL || numkeys == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    for (i = 0; i != num; i++) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
+        if (ee == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        count += (ee->keyshare != NULL);
+    }
+    *numkeys = count;
+    return 1;
+}
+
+int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age)
+{
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    int i, num = 0;
+    time_t now = time(0);
+
+    if (es == NULL) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+    num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
+    if (num == 0) {
+        ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+        return 0;
+    }
+    for (i = num - 1; i >= 0; i--) {
+        ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
+        if (ee == NULL) {
+            ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
+            return 0;
+        }
+        if (ee->keyshare != NULL && ee->loadtime + age <= now) {
+            ossl_echstore_entry_free(ee);
+            sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i);
+        }
+    }
+    return 1;
+}
diff --git a/ssl/ssl_ciph.c b/ssl/ssl_ciph.c
index 2c35f31065cf2..eee0dd14e61d8 100644
--- a/ssl/ssl_ciph.c
+++ b/ssl/ssl_ciph.c
@@ -2262,3 +2262,107 @@ const char *OSSL_default_ciphersuites(void)
            "TLS_CHACHA20_POLY1305_SHA256:"
            "TLS_AES_128_GCM_SHA256";
 }
+
+int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
+                             WPACKET *pkt)
+{
+    int i;
+    size_t totlen = 0, len, maxlen, maxverok = 0;
+    int empty_reneg_info_scsv = !s->renegotiate
+                                && !SSL_CONNECTION_IS_DTLS(s)
+                                && ssl_security(s, SSL_SECOP_VERSION, 0, TLS1_VERSION, NULL)
+                                && s->min_proto_version <= TLS1_VERSION;
+    SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+
+    /* Set disabled masks for this session */
+    if (!ssl_set_client_disabled(s)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_PROTOCOLS_AVAILABLE);
+        return 0;
+    }
+
+    if (sk == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+
+#ifdef OPENSSL_MAX_TLS1_2_CIPHER_LENGTH
+# if OPENSSL_MAX_TLS1_2_CIPHER_LENGTH < 6
+#  error Max cipher length too short
+# endif
+    /*
+     * Some servers hang if client hello > 256 bytes as hack workaround
+     * chop number of supported ciphers to keep it well below this if we
+     * use TLS v1.2
+     */
+    if (TLS1_get_version(ssl) >= TLS1_2_VERSION)
+        maxlen = OPENSSL_MAX_TLS1_2_CIPHER_LENGTH & ~1;
+    else
+#endif
+        /* Maximum length that can be stored in 2 bytes. Length must be even */
+        maxlen = 0xfffe;
+
+    if (empty_reneg_info_scsv)
+        maxlen -= 2;
+    if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV)
+        maxlen -= 2;
+
+    for (i = 0; i < sk_SSL_CIPHER_num(sk) && totlen < maxlen; i++) {
+        const SSL_CIPHER *c;
+
+        c = sk_SSL_CIPHER_value(sk, i);
+        /* Skip disabled ciphers */
+        if (ssl_cipher_disabled(s, c, SSL_SECOP_CIPHER_SUPPORTED, 0))
+            continue;
+
+        if (!ssl->method->put_cipher_by_char(c, pkt, &len)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+
+        /* Sanity check that the maximum version we offer has ciphers enabled */
+        if (!maxverok) {
+            int minproto = SSL_CONNECTION_IS_DTLS(s) ? c->min_dtls : c->min_tls;
+            int maxproto = SSL_CONNECTION_IS_DTLS(s) ? c->max_dtls : c->max_tls;
+
+            if (ssl_version_cmp(s, maxproto, s->s3.tmp.max_ver) >= 0
+                    && ssl_version_cmp(s, minproto, s->s3.tmp.max_ver) <= 0)
+                maxverok = 1;
+        }
+
+        totlen += len;
+    }
+
+    if (totlen == 0 || !maxverok) {
+        const char *maxvertext =
+            !maxverok
+            ? "No ciphers enabled for max supported SSL/TLS version"
+            : NULL;
+
+        SSLfatal_data(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_CIPHERS_AVAILABLE,
+                      maxvertext);
+        return 0;
+    }
+
+    if (totlen != 0) {
+        if (empty_reneg_info_scsv) {
+            static const SSL_CIPHER scsv = {
+                0, NULL, NULL, SSL3_CK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0
+            };
+            if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return 0;
+            }
+        }
+        if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) {
+            static const SSL_CIPHER scsv = {
+                0, NULL, NULL, SSL3_CK_FALLBACK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0
+            };
+            if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return 0;
+            }
+        }
+    }
+
+    return 1;
+}
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 951723748ffd3..63d3726931b4a 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -968,6 +968,11 @@ SSL *ossl_ssl_connection_new_int(SSL_CTX *ctx, SSL *user_ssl,
         goto sslerr;
 #endif
 
+#ifndef OPENSSL_NO_ECH
+    if (!ossl_ech_conn_init(s, ctx, method))
+        goto sslerr;
+#endif
+
     s->ssl_pkey_num = SSL_PKEY_NUM + ctx->sigalg_list_len;
     return ssl;
  cerr:
@@ -1552,6 +1557,9 @@ void ossl_ssl_connection_free(SSL *ssl)
     BIO_free_all(s->rbio);
     s->rbio = NULL;
     OPENSSL_free(s->s3.tmp.valid_flags);
+#ifndef OPENSSL_NO_ECH
+    ossl_ech_conn_clear(&s->ext.ech);
+#endif
 }
 
 void SSL_set0_rbio(SSL *s, BIO *rbio)
@@ -4480,6 +4488,10 @@ void SSL_CTX_free(SSL_CTX *a)
     ossl_quic_free_token_store(a->tokencache);
 #endif
 
+#ifndef OPENSSL_NO_ECH
+    ossl_ech_ctx_clear(&a->ext.ech);
+#endif
+
     OPENSSL_free(a);
 }
 
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index 5cf7368a82ccb..36410ea806a9f 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -41,6 +41,9 @@
 # include "record/record.h"
 # include "internal/quic_predef.h"
 # include "internal/quic_tls.h"
+# ifndef OPENSSL_NO_ECH
+#  include "ech/ech_local.h"
+# endif
 
 # ifdef OPENSSL_BUILD_SHLIBSSL
 #  undef OPENSSL_EXTERN
@@ -688,6 +691,8 @@ typedef enum tlsext_index_en {
     TLSEXT_IDX_compress_certificate,
     TLSEXT_IDX_early_data,
     TLSEXT_IDX_certificate_authorities,
+    TLSEXT_IDX_ech,
+    TLSEXT_IDX_outer_extensions,
     TLSEXT_IDX_padding,
     TLSEXT_IDX_psk,
     /* Dummy index - must always be the last entry */
@@ -787,11 +792,6 @@ typedef struct {
 
 # define TLS_GROUP_FFDHE_FOR_TLS1_3 (TLS_GROUP_FFDHE|TLS_GROUP_ONLY_FOR_TLS1_3)
 
-/* We limit the number of key shares sent */
-# ifndef OPENSSL_CLIENT_MAX_KEY_SHARES
-#  define OPENSSL_CLIENT_MAX_KEY_SHARES 4
-# endif
-
 struct ssl_ctx_st {
     OSSL_LIB_CTX *libctx;
 
@@ -1078,6 +1078,9 @@ struct ssl_ctx_st {
 # endif
 
         unsigned char cookie_hmac_key[SHA256_DIGEST_LENGTH];
+# ifndef OPENSSL_NO_ECH
+        OSSL_ECH_CTX ech;
+# endif
     } ext;
 
 # ifndef OPENSSL_NO_PSK
@@ -1736,6 +1739,10 @@ struct ssl_connection_st {
         uint8_t client_cert_type_ctos;
         uint8_t server_cert_type;
         uint8_t server_cert_type_ctos;
+
+# ifndef OPENSSL_NO_ECH
+        OSSL_ECH_CONN ech;
+# endif
     } ext;
 
     /*
@@ -2623,6 +2630,8 @@ __owur STACK_OF(SSL_CIPHER) *ssl_get_ciphers_by_id(SSL_CONNECTION *sc);
 __owur int ssl_x509err2alert(int type);
 void ssl_sort_cipher_list(void);
 int ssl_load_ciphers(SSL_CTX *ctx);
+int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
+                             WPACKET *pkt);
 __owur int ssl_setup_sigalgs(SSL_CTX *ctx);
 int ssl_load_groups(SSL_CTX *ctx);
 int ssl_load_sigalgs(SSL_CTX *ctx);
diff --git a/ssl/ssl_stat.c b/ssl/ssl_stat.c
index d6ba000c65d45..01e0fcd3b0550 100644
--- a/ssl/ssl_stat.c
+++ b/ssl/ssl_stat.c
@@ -333,6 +333,10 @@ const char *SSL_alert_desc_string(int value)
         return "BH";
     case TLS1_AD_UNKNOWN_PSK_IDENTITY:
         return "UP";
+#ifndef OPENSSL_NO_ECH
+    case TLS1_AD_ECH_REQUIRED:
+        return "RR";
+#endif
     default:
         return "UK";
     }
@@ -403,6 +407,10 @@ const char *SSL_alert_desc_string_long(int value)
         return "unknown PSK identity";
     case TLS1_AD_NO_APPLICATION_PROTOCOL:
         return "no application protocol";
+#ifndef OPENSSL_NO_ECH
+    case TLS1_AD_ECH_REQUIRED:
+        return "ECH required";
+#endif
     default:
         return "unknown";
     }
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index 59587fedbe9b6..4bd3e677e0886 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -21,6 +21,40 @@
 #include 
 #include 
 
+/*
+ * values for ext_defs ech_handling field
+ * exceptionally, we don't conditionally compile that field to avoid a pile of
+ * ifndefs all over the ext_defs values
+ */
+#define OSSL_ECH_HANDLING_CALL_BOTH 1 /* call constructor both times */
+#define OSSL_ECH_HANDLING_COMPRESS  2 /* compress outer value into inner */
+#define OSSL_ECH_HANDLING_DUPLICATE 3 /* same value in inner and outer */
+/*
+ * DUPLICATE isn't really useful other than to show we can,
+ * and for debugging/tests/coverage so may disappear. Changes mostly
+ * won't affect the outer CH size, due to padding, but might for some
+ * larger extensions.
+ *
+ * Note there is a co-dependency with test/recipes/75-test_quicapi.t:
+ * If you change an |ech_handling| value, that may well affect the order
+ * of extensions in a ClientHello, which is reflected in the test data
+ * in test/recipes/75-test_quicapi_data/\*.txt files. To fix, you need
+ * to look in test-runs/test_quicapi for the "new" files and then edit
+ * (replacing actual octets with "?" in relevant places), and copy the
+ * result back over to test/recipes/75-test_quicapi_data/. The reason
+ * this happens is the ECH COMPRESS'd extensions need to be contiguous
+ * in the ClientHello, so changes to/from COMPRESS affect extension
+ * order, in inner and outer CH. There doesn't seem to be an easy,
+ * generic, way to reconcile these compile-time changes with having
+ * fixed value test files. Likely the best option is to decide on the
+ * disposition of ECH COMPRESS or not and consider that an at least
+ * medium-term thing. (But still allow other builds to vary at
+ * compile time if they need something different.)
+ */
+#ifndef OPENSSL_NO_ECH
+static int init_ech(SSL_CONNECTION *s, unsigned int context);
+#endif /* OPENSSL_NO_ECH */
+
 static int final_renegotiate(SSL_CONNECTION *s, unsigned int context, int sent);
 static int init_server_name(SSL_CONNECTION *s, unsigned int context);
 static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent);
@@ -86,6 +120,11 @@ typedef struct extensions_definition_st {
      * protocol versions
      */
     unsigned int context;
+    /*
+     * exceptionally, we don't conditionally compile this field to avoid a
+     * pile of ifndefs all over the ext_defs values
+     */
+    int ech_handling;  /* how to handle ECH for this extension type */
     /*
      * Initialise extension before parsing. Always called for relevant contexts
      * even if extension not present
@@ -140,12 +179,14 @@ typedef struct extensions_definition_st {
  * NOTE: WebSphere Application Server 7+ cannot handle empty extensions at
  * the end, keep these extensions before signature_algorithm.
  */
-#define INVALID_EXTENSION { TLSEXT_TYPE_invalid, 0, NULL, NULL, NULL, NULL, NULL, NULL }
+#define INVALID_EXTENSION { TLSEXT_TYPE_invalid, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL }
+
 static const EXTENSION_DEFINITION ext_defs[] = {
     {
         TLSEXT_TYPE_renegotiate,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_SSL3_ALLOWED | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, tls_parse_ctos_renegotiate, tls_parse_stoc_renegotiate,
         tls_construct_stoc_renegotiate, tls_construct_ctos_renegotiate,
         final_renegotiate
@@ -154,6 +195,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_server_name,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         init_server_name,
         tls_parse_ctos_server_name, tls_parse_stoc_server_name,
         tls_construct_stoc_server_name, tls_construct_ctos_server_name,
@@ -163,6 +205,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_max_fragment_length,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, tls_parse_ctos_maxfragmentlen, tls_parse_stoc_maxfragmentlen,
         tls_construct_stoc_maxfragmentlen, tls_construct_ctos_maxfragmentlen,
         final_maxfragmentlen
@@ -171,6 +214,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
     {
         TLSEXT_TYPE_srp,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_srp, tls_parse_ctos_srp, NULL, NULL, tls_construct_ctos_srp, NULL
     },
 #else
@@ -180,6 +224,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_ec_point_formats,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_ec_point_formats, tls_parse_ctos_ec_pt_formats, tls_parse_stoc_ec_pt_formats,
         tls_construct_stoc_ec_pt_formats, tls_construct_ctos_ec_pt_formats,
         final_ec_pt_formats
@@ -213,6 +258,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_supported_groups,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS
         | SSL_EXT_TLS1_2_SERVER_HELLO,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, tls_parse_ctos_supported_groups, NULL,
         tls_construct_stoc_supported_groups,
         tls_construct_ctos_supported_groups, NULL
@@ -221,6 +267,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_session_ticket,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_session_ticket, tls_parse_ctos_session_ticket,
         tls_parse_stoc_session_ticket, tls_construct_stoc_session_ticket,
         tls_construct_ctos_session_ticket, NULL
@@ -230,6 +277,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_status_request,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_status_request, tls_parse_ctos_status_request,
         tls_parse_stoc_status_request, tls_construct_stoc_status_request,
         tls_construct_ctos_status_request, NULL
@@ -242,6 +290,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_next_proto_neg,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_npn, tls_parse_ctos_npn, tls_parse_stoc_npn,
         tls_construct_stoc_next_proto_neg, tls_construct_ctos_npn, NULL
     },
@@ -256,6 +305,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_application_layer_protocol_negotiation,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         init_alpn, tls_parse_ctos_alpn, tls_parse_stoc_alpn,
         tls_construct_stoc_alpn, tls_construct_ctos_alpn, final_alpn
     },
@@ -264,6 +314,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_use_srtp,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_DTLS_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_srtp, tls_parse_ctos_use_srtp, tls_parse_stoc_use_srtp,
         tls_construct_stoc_use_srtp, tls_construct_ctos_use_srtp, NULL
     },
@@ -274,6 +325,15 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_encrypt_then_mac,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        /*
+         * If you want to demonstrate/exercise duplicate, then
+         * this does that and has no effect on sizes, but it
+         * will break the quicapi test (see above). Probably
+         * best done in local tests and not committed to any
+         * upstream.
+         * OSSL_ECH_HANDLING_DUPLICATE,
+         */
+        OSSL_ECH_HANDLING_COMPRESS,
         init_etm, tls_parse_ctos_etm, tls_parse_stoc_etm,
         tls_construct_stoc_etm, tls_construct_ctos_etm, NULL
     },
@@ -282,6 +342,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_signed_certificate_timestamp,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL,
         /*
          * No server side support for this, but can be provided by a custom
@@ -297,12 +358,14 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_extended_master_secret,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_ems, tls_parse_ctos_ems, tls_parse_stoc_ems,
         tls_construct_stoc_ems, tls_construct_ctos_ems, final_ems
     },
     {
         TLSEXT_TYPE_signature_algorithms_cert,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_sig_algs_cert, tls_parse_ctos_sig_algs_cert,
         tls_parse_ctos_sig_algs_cert,
         /* We do not generate signature_algorithms_cert at present. */
@@ -311,6 +374,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
     {
         TLSEXT_TYPE_post_handshake_auth,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_post_handshake_auth,
         tls_parse_ctos_post_handshake_auth, NULL,
         NULL, tls_construct_ctos_post_handshake_auth,
@@ -320,6 +384,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_client_cert_type,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS
         | SSL_EXT_TLS1_2_SERVER_HELLO,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         init_client_cert_type,
         tls_parse_ctos_client_cert_type, tls_parse_stoc_client_cert_type,
         tls_construct_stoc_client_cert_type, tls_construct_ctos_client_cert_type,
@@ -329,6 +394,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_server_cert_type,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS
         | SSL_EXT_TLS1_2_SERVER_HELLO,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         init_server_cert_type,
         tls_parse_ctos_server_cert_type, tls_parse_stoc_server_cert_type,
         tls_construct_stoc_server_cert_type, tls_construct_ctos_server_cert_type,
@@ -337,6 +403,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
     {
         TLSEXT_TYPE_signature_algorithms,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_sig_algs, tls_parse_ctos_sig_algs,
         tls_parse_ctos_sig_algs, tls_construct_ctos_sig_algs,
         tls_construct_ctos_sig_algs, final_sig_algs
@@ -345,6 +412,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_supported_versions,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO
         | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL,
         /* Processed inline as part of version selection */
         NULL, tls_parse_stoc_supported_versions,
@@ -355,6 +423,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_psk_kex_modes,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS_IMPLEMENTATION_ONLY
         | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_psk_kex_modes, tls_parse_ctos_psk_kex_modes, NULL, NULL,
         tls_construct_ctos_psk_kex_modes, NULL
     },
@@ -367,6 +436,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO
         | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY
         | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, tls_parse_ctos_key_share, tls_parse_stoc_key_share,
         tls_construct_stoc_key_share, tls_construct_ctos_key_share,
         final_key_share
@@ -376,6 +446,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_cookie,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST
         | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, tls_parse_ctos_cookie, tls_parse_stoc_cookie,
         tls_construct_stoc_cookie, tls_construct_ctos_cookie, NULL
     },
@@ -388,12 +459,14 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_cryptopro_bug,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO
         | SSL_EXT_TLS1_2_AND_BELOW_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         NULL, NULL, NULL, tls_construct_stoc_cryptopro_bug, NULL, NULL
     },
     {
         TLSEXT_TYPE_compress_certificate,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST
         | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         tls_init_compress_certificate,
         tls_parse_compress_certificate, tls_parse_compress_certificate,
         tls_construct_compress_certificate, tls_construct_compress_certificate,
@@ -403,6 +476,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_early_data,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS
         | SSL_EXT_TLS1_3_NEW_SESSION_TICKET | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         NULL, tls_parse_ctos_early_data, tls_parse_stoc_early_data,
         tls_construct_stoc_early_data, tls_construct_ctos_early_data,
         final_early_data
@@ -411,15 +485,42 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_certificate_authorities,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST
         | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_COMPRESS,
         init_certificate_authorities,
         tls_parse_certificate_authorities, tls_parse_certificate_authorities,
         tls_construct_certificate_authorities,
         tls_construct_certificate_authorities, NULL,
     },
+#ifndef OPENSSL_NO_ECH
+    {
+        TLSEXT_TYPE_ech,
+        SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY |
+        SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS |
+        SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
+        OSSL_ECH_HANDLING_CALL_BOTH,
+        init_ech,
+        tls_parse_ctos_ech, tls_parse_stoc_ech,
+        tls_construct_stoc_ech, tls_construct_ctos_ech,
+        NULL
+    },
+    {
+        TLSEXT_TYPE_outer_extensions,
+        SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_CALL_BOTH,
+        NULL,
+        NULL, NULL,
+        NULL, NULL,
+        NULL
+    },
+#else /* OPENSSL_NO_ECH */
+    INVALID_EXTENSION,
+    INVALID_EXTENSION,
+#endif /* END_OPENSSL_NO_ECH */
     {
         /* Must be immediately before pre_shared_key */
         TLSEXT_TYPE_padding,
         SSL_EXT_CLIENT_HELLO,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         NULL,
         /* We send this, but don't read it */
         NULL, NULL, NULL, tls_construct_ctos_padding, NULL
@@ -429,11 +530,137 @@ static const EXTENSION_DEFINITION ext_defs[] = {
         TLSEXT_TYPE_psk,
         SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO
         | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY,
+        OSSL_ECH_HANDLING_CALL_BOTH,
         NULL, tls_parse_ctos_psk, tls_parse_stoc_psk, tls_construct_stoc_psk,
         tls_construct_ctos_psk, final_psk
     }
 };
 
+#ifndef OPENSSL_NO_ECH
+/*
+ * Copy an inner extension value to outer.
+ * inner CH must have been pre-decoded into s->clienthello->pre_proc_exts
+ * already.
+ */
+int ossl_ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type,
+                              int ind, WPACKET *pkt)
+{
+    RAW_EXTENSION *myext = NULL, *raws = NULL;
+
+    if (s == NULL || s->clienthello == NULL)
+        return OSSL_ECH_SAME_EXT_ERR;
+    raws = s->clienthello->pre_proc_exts;
+    if (raws == NULL)
+        return OSSL_ECH_SAME_EXT_ERR;
+    myext = &raws[ind];
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "inner2outer: Copying ext type %d to outer\n",
+                   ext_type);
+    } OSSL_TRACE_END(TLS);
+    /*
+     * This one wasn't in inner, so re-do processing. We don't
+     * actually do this currently, but could.
+     */
+    if (myext == NULL)
+        return OSSL_ECH_SAME_EXT_CONTINUE;
+    /* copy inner value to outer */
+    if (PACKET_data(&myext->data) != NULL
+        && PACKET_remaining(&myext->data) > 0) {
+        if (!WPACKET_put_bytes_u16(pkt, ext_type)
+            || !WPACKET_sub_memcpy_u16(pkt, PACKET_data(&myext->data),
+                                       PACKET_remaining(&myext->data)))
+            return OSSL_ECH_SAME_EXT_ERR;
+    } else {
+        /* empty extension */
+        if (!WPACKET_put_bytes_u16(pkt, ext_type)
+            || !WPACKET_put_bytes_u16(pkt, 0))
+            return OSSL_ECH_SAME_EXT_ERR;
+    }
+    return 1;
+}
+
+/*
+ * DUPEMALL is useful for testing - this turns off compression and
+ * causes two calls to each extension constructor, which'd be the same
+ * as making all entries in ext_tab use the CALL_BOTH value
+ */
+# undef DUPEMALL
+
+/*
+ * Check if we're using the same/different key shares
+ * return 1 if same key share in inner and outer, 0 otherwise
+ */
+int ossl_ech_same_key_share(void)
+{
+# ifdef DUPEMALL
+    return 0;
+# endif
+    return ext_defs[TLSEXT_IDX_key_share].ech_handling
+        != OSSL_ECH_HANDLING_CALL_BOTH;
+}
+
+/*
+ * say if extension at index |ind| in ext_defs is to be ECH compressed
+ * return 1 if this one is to be compressed, 0 if not, -1 for error
+ */
+int ossl_ech_2bcompressed(size_t ind)
+{
+    const size_t nexts = OSSL_NELEM(ext_defs);
+
+# ifdef DUPEMALL
+    return 0;
+# endif
+    if (ind < 0 || ind >= nexts)
+        return -1;
+    return ext_defs[ind].ech_handling == OSSL_ECH_HANDLING_COMPRESS;
+}
+
+/* as needed, repeat extension from inner in outer handling compression */
+int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt)
+{
+    unsigned int type = 0;
+    int tind = 0, nexts = OSSL_NELEM(ext_defs);
+
+# ifdef DUPEMALL
+    return OSSL_ECH_SAME_EXT_CONTINUE;
+# 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 */
+    tind = s->ext.ech.ext_ind;
+    /* If this index'd extension won't be compressed, we're done */
+    if (tind < 0 || tind >= nexts)
+        return OSSL_ECH_SAME_EXT_ERR;
+    type = ext_defs[tind].type;
+    if (s->ext.ech.ch_depth == 1) {
+        /* inner CH - just note compression as configured */
+        if (ext_defs[tind].ech_handling != OSSL_ECH_HANDLING_COMPRESS)
+            return OSSL_ECH_SAME_EXT_CONTINUE;
+        /* mark this one to be "compressed" */
+        if (s->ext.ech.n_outer_only >= OSSL_ECH_OUTERS_MAX)
+            return OSSL_ECH_SAME_EXT_ERR;
+        s->ext.ech.outer_only[s->ext.ech.n_outer_only] = type;
+        s->ext.ech.n_outer_only++;
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "ech_same_ext: Marking (type %u, ind %d "
+                       "tot-comp %d) for compression\n", type, tind,
+                       (int) s->ext.ech.n_outer_only);
+        } OSSL_TRACE_END(TLS);
+        return OSSL_ECH_SAME_EXT_CONTINUE;
+    } else {
+        /* Copy value from inner to outer, or indicate a new value needed */
+        if (s->clienthello == NULL || pkt == NULL)
+            return OSSL_ECH_SAME_EXT_ERR;
+        if (ext_defs[tind].ech_handling == OSSL_ECH_HANDLING_CALL_BOTH)
+            return OSSL_ECH_SAME_EXT_CONTINUE;
+        else
+            return ossl_ech_copy_inner2outer(s, type, tind, pkt);
+    }
+    /* just in case - shouldn't happen */
+    return OSSL_ECH_SAME_EXT_ERR;
+}
+#endif
+
 /* Returns a TLSEXT_TYPE for the given index */
 unsigned int ossl_get_extension_type(size_t idx)
 {
@@ -858,6 +1085,9 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt,
     int min_version, max_version = 0, reason;
     const EXTENSION_DEFINITION *thisexd;
     int for_comp = (context & SSL_EXT_TLS1_3_CERTIFICATE_COMPRESSION) != 0;
+#ifndef OPENSSL_NO_ECH
+    int pass;
+#endif
 
     if (!WPACKET_start_sub_packet_u16(pkt)
                /*
@@ -893,40 +1123,78 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt,
         return 0;
     }
 
-    for (i = 0, thisexd = ext_defs; i < OSSL_NELEM(ext_defs); i++, thisexd++) {
-        EXT_RETURN (*construct)(SSL_CONNECTION *s, WPACKET *pkt,
-                                unsigned int context,
-                                X509 *x, size_t chainidx);
-        EXT_RETURN ret;
+#ifndef OPENSSL_NO_ECH
+    /*
+     * Two passes if doing real ECH - we first construct the
+     * to-be-ECH-compressed extensions, and then go around again
+     * constructing those that aren't to be ECH-compressed. We
+     * need to ensure this ordering so that all the ECH-compressed
+     * extensions are contiguous in the encoding. The actual
+     * compression happens later in ech_encode_inner().
+     */
+    for (pass = 0; pass <= 1; pass++)
+#endif
 
-        /* Skip if not relevant for our context */
-        if (!should_add_extension(s, thisexd->context, context, max_version))
-            continue;
+        for (i = 0, thisexd = ext_defs; i < OSSL_NELEM(ext_defs);
+             i++, thisexd++) {
+            EXT_RETURN (*construct)(SSL_CONNECTION *s, WPACKET *pkt,
+                                   unsigned int context,
+                                   X509 *x, size_t chainidx);
+            EXT_RETURN ret;
+
+#ifndef OPENSSL_NO_ECH
+            /* do compressed in pass 0, non-compressed in pass 1 */
+            if (ossl_ech_2bcompressed((int)i) == pass)
+                continue;
+            /* stash index - needed for COMPRESS ECH handling */
+            s->ext.ech.ext_ind = (int)i;
+#endif
+            /* Skip if not relevant for our context */
+            if (!should_add_extension(s, thisexd->context, context, max_version))
+                continue;
 
-        construct = s->server ? thisexd->construct_stoc
-                              : thisexd->construct_ctos;
+            construct = s->server ? thisexd->construct_stoc
+                                  : thisexd->construct_ctos;
 
-        if (construct == NULL)
-            continue;
+            if (construct == NULL)
+                continue;
 
-        ret = construct(s, pkt, context, x, chainidx);
-        if (ret == EXT_RETURN_FAIL) {
-            /* SSLfatal() already called */
+            ret = construct(s, pkt, context, x, chainidx);
+            if (ret == EXT_RETURN_FAIL) {
+                /* SSLfatal() already called */
+                return 0;
+            }
+            if (ret == EXT_RETURN_SENT
+                    && (context & (SSL_EXT_CLIENT_HELLO
+                                   | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST
+                                   | SSL_EXT_TLS1_3_NEW_SESSION_TICKET)) != 0)
+                s->ext.extflags[i] |= SSL_EXT_FLAG_SENT;
+        }
+
+#ifndef OPENSSL_NO_ECH
+    /*
+     * don't close yet if client in the middle of doing ECH, we'll
+     * eventually close this in ech_aad_and_encrypt() after we add
+     * the real ECH extension value
+     */
+    if (s->server
+        || context != SSL_EXT_CLIENT_HELLO
+        || s->ext.ech.attempted == 0
+        || s->ext.ech.ch_depth == 1
+        || s->ext.ech.grease == OSSL_ECH_IS_GREASE) {
+        if (!WPACKET_close(pkt)) {
+            if (!for_comp)
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return 0;
         }
-        if (ret == EXT_RETURN_SENT
-                && (context & (SSL_EXT_CLIENT_HELLO
-                               | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST
-                               | SSL_EXT_TLS1_3_NEW_SESSION_TICKET)) != 0)
-            s->ext.extflags[i] |= SSL_EXT_FLAG_SENT;
     }
-
+#else
     if (!WPACKET_close(pkt)) {
         if (!for_comp)
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return 0;
     }
-
+#endif
     return 1;
 }
 
@@ -989,6 +1257,26 @@ static int init_server_name(SSL_CONNECTION *s, unsigned int context)
     return 1;
 }
 
+#ifndef OPENSSL_NO_ECH
+/*
+ * Just note that ech is not yet done
+ * return 1 for good, 0 otherwise
+ */
+static int init_ech(SSL_CONNECTION *s, unsigned int context)
+{
+    const int nexts = OSSL_NELEM(ext_defs);
+
+    /* we don't need this assert everywhere - anywhere is fine */
+    if (!ossl_assert(TLSEXT_IDX_num_builtins == nexts)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if ((context & SSL_EXT_CLIENT_HELLO) != 0)
+        s->ext.ech.done = 0;
+    return 1;
+}
+#endif /* OPENSSL_NO_ECH */
+
 static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent)
 {
     int ret = SSL_TLSEXT_ERR_NOACK;
@@ -1050,7 +1338,7 @@ static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent)
             && was_ticket && (SSL_get_options(ssl) & SSL_OP_NO_TICKET) != 0) {
         s->ext.ticket_expected = 0;
         if (!s->hit) {
-            SSL_SESSION* ss = SSL_get_session(ssl);
+            SSL_SESSION *ss = SSL_get_session(ssl);
 
             if (ss != NULL) {
                 OPENSSL_free(ss->ext.tick);
@@ -1619,12 +1907,24 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md,
         long hdatalen_l;
         void *hdata;
 
-        hdatalen = hdatalen_l =
-            BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
-        if (hdatalen_l <= 0) {
-            SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH);
-            goto err;
+#ifndef OPENSSL_NO_ECH
+        /* handle the hashing as per ECH needs (on client) */
+        if (s->ext.ech.attempted == 1 && s->ext.ech.ch_depth == 1) {
+            if (ossl_ech_intbuf_fetch(s, (unsigned char **)&hdata, &hdatalen) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        } else {
+#endif
+            hdatalen = hdatalen_l =
+                BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
+            if (hdatalen_l <= 0) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH);
+                goto err;
+            }
+#ifndef OPENSSL_NO_ECH
         }
+#endif
 
         /*
          * For servers the handshake buffer data will include the second
@@ -1695,7 +1995,6 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md,
     OPENSSL_cleanse(finishedkey, sizeof(finishedkey));
     EVP_PKEY_free(mackey);
     EVP_MD_CTX_free(mctx);
-
     return ret;
 }
 
@@ -1822,6 +2121,9 @@ static EXT_RETURN tls_construct_compress_certificate(SSL_CONNECTION *sc, WPACKET
 
     if (sc->cert_comp_prefs[0] == TLSEXT_comp_cert_none)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(sc, context, pkt);
+# endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_compress_certificate)
             || !WPACKET_start_sub_packet_u16(pkt)
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 4e89c963ddc13..18c4ed548d464 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -12,6 +12,10 @@
 #include "internal/cryptlib.h"
 #include "internal/ssl_unwrap.h"
 #include "statem_local.h"
+#ifndef OPENSSL_NO_ECH
+# include 
+# include "internal/ech_helpers.h"
+#endif
 
 EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt,
                                           unsigned int context, X509 *x,
@@ -35,6 +39,9 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt,
             return EXT_RETURN_NOT_SENT;
         }
 
+#ifndef OPENSSL_NO_ECH
+        ECH_SAME_EXT(s, context, pkt)
+#endif
 
         if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_renegotiate)
             || !WPACKET_start_sub_packet_u16(pkt)
@@ -47,6 +54,10 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt,
         return EXT_RETURN_SENT;
     }
 
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
+
     /* Add a complete RI extension if renegotiating */
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_renegotiate)
             || !WPACKET_start_sub_packet_u16(pkt)
@@ -64,9 +75,31 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt,
                                           unsigned int context, X509 *x,
                                           size_t chainidx)
 {
-    if (s->ext.hostname == NULL)
-        return EXT_RETURN_NOT_SENT;
+    char *chosen = s->ext.hostname;
+#ifndef OPENSSL_NO_ECH
+    OSSL_HPKE_SUITE suite;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
 
+    if (s->ext.ech.es != NULL) {
+        if (ossl_ech_pick_matching_cfg(s, &ee, &suite) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_NOT_SENT;
+        }
+        /* Don't send outer SNI if external API says so */
+        if (s->ext.ech.ch_depth == 0 && s->ext.ech.no_outer == 1)
+            return EXT_RETURN_NOT_SENT;
+        if (s->ext.ech.ch_depth == 1) /* inner */
+            chosen = s->ext.hostname;
+        if (s->ext.ech.ch_depth == 0) { /* outer */
+            if (s->ext.ech.outer_hostname != NULL) /* prefer API */
+                chosen = s->ext.ech.outer_hostname;
+            else /* use name from ECHConfig */
+                chosen = ee->public_name;
+        }
+    }
+#endif
+    if (chosen == NULL)
+        return EXT_RETURN_NOT_SENT;
     /* Add TLS extension servername to the Client Hello message */
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_name)
                /* Sub-packet for server_name extension */
@@ -74,14 +107,12 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt,
                /* Sub-packet for servername list (always 1 hostname)*/
             || !WPACKET_start_sub_packet_u16(pkt)
             || !WPACKET_put_bytes_u8(pkt, TLSEXT_NAMETYPE_host_name)
-            || !WPACKET_sub_memcpy_u16(pkt, s->ext.hostname,
-                                       strlen(s->ext.hostname))
+            || !WPACKET_sub_memcpy_u16(pkt, chosen, strlen(chosen))
             || !WPACKET_close(pkt)
             || !WPACKET_close(pkt)) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return EXT_RETURN_FAIL;
     }
-
     return EXT_RETURN_SENT;
 }
 
@@ -92,6 +123,9 @@ EXT_RETURN tls_construct_ctos_maxfragmentlen(SSL_CONNECTION *s, WPACKET *pkt,
 {
     if (s->ext.max_fragment_len_mode == TLSEXT_max_fragment_length_DISABLED)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     /* Add Max Fragment Length extension if client enabled it. */
     /*-
@@ -118,6 +152,9 @@ EXT_RETURN tls_construct_ctos_srp(SSL_CONNECTION *s, WPACKET *pkt,
     /* Add SRP username if there is one */
     if (s->srp_ctx.login == NULL)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_srp)
                /* Sub-packet for SRP extension */
@@ -196,6 +233,9 @@ EXT_RETURN tls_construct_ctos_ec_pt_formats(SSL_CONNECTION *s, WPACKET *pkt,
     }
     if (!use_ecc(s, min_version, max_version))
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     /* Add TLS extension ECPointFormats to the ClientHello message */
     tls1_get_formatlist(s, &pformats, &num_formats);
@@ -233,6 +273,9 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
     if (!use_ecc(s, min_version, max_version)
             && (SSL_CONNECTION_IS_DTLS(s) || max_version < TLS1_3_VERSION))
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     /*
      * Add TLS extension supported_groups to the ClientHello message
@@ -289,6 +332,9 @@ EXT_RETURN tls_construct_ctos_session_ticket(SSL_CONNECTION *s, WPACKET *pkt,
 
     if (!tls_use_ticket(s))
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     if (!s->new_session && s->session != NULL
             && s->session->ext.tick != NULL
@@ -346,6 +392,10 @@ EXT_RETURN tls_construct_ctos_sig_algs(SSL_CONNECTION *s, WPACKET *pkt,
         return EXT_RETURN_NOT_SENT;
     }
 
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
+
     salglen = tls12_get_psigalgs(s, 1, &salg);
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_signature_algorithms)
                /* Sub-packet for sig-algs extension */
@@ -375,6 +425,9 @@ EXT_RETURN tls_construct_ctos_status_request(SSL_CONNECTION *s, WPACKET *pkt,
 
     if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_status_request)
                /* Sub-packet for status request extension */
@@ -435,6 +488,9 @@ EXT_RETURN tls_construct_ctos_npn(SSL_CONNECTION *s, WPACKET *pkt,
     if (SSL_CONNECTION_GET_CTX(s)->ext.npn_select_cb == NULL
         || !SSL_IS_FIRST_HANDSHAKE(s))
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     /*
      * The client advertises an empty extension to indicate its support
@@ -454,22 +510,46 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt,
                                    unsigned int context,
                                    X509 *x, size_t chainidx)
 {
-    s->s3.alpn_sent = 0;
+    unsigned char *aval = s->ext.alpn;
+    size_t alen = s->ext.alpn_len;
 
-    if (s->ext.alpn == NULL || !SSL_IS_FIRST_HANDSHAKE(s))
+    s->s3.alpn_sent = 0;
+    if (!SSL_IS_FIRST_HANDSHAKE(s))
+        return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    /*
+     * If we have different alpn and alpn_outer values, then we set
+     * the appropriate one for inner and outer.
+     * If no alpn is set (for inner or outer), we don't send any.
+     * If only an inner is set then we send the same in both.
+     * Logic above is on the basis that alpn's aren't that sensitive,
+     * usually, so special action is needed to do better.
+     * We also don't support a way to send alpn only in the inner.
+     * If you don't want the inner value in the outer, you have to
+     * pick what to send in the outer and send that.
+     */
+    if (s->ext.ech.ch_depth == 1 && s->ext.alpn == NULL)  /* inner */
+        return EXT_RETURN_NOT_SENT;
+    if (s->ext.ech.ch_depth == 0 && s->ext.alpn == NULL
+        && s->ext.ech.alpn_outer == NULL) /* outer */
+        return EXT_RETURN_NOT_SENT;
+    if (s->ext.ech.ch_depth == 0 && s->ext.ech.alpn_outer != NULL) {
+        aval = s->ext.ech.alpn_outer;
+        alen = s->ext.ech.alpn_outer_len;
+    }
+#endif
+    if (aval == NULL)
         return EXT_RETURN_NOT_SENT;
-
     if (!WPACKET_put_bytes_u16(pkt,
                 TLSEXT_TYPE_application_layer_protocol_negotiation)
-               /* Sub-packet ALPN extension */
-            || !WPACKET_start_sub_packet_u16(pkt)
-            || !WPACKET_sub_memcpy_u16(pkt, s->ext.alpn, s->ext.alpn_len)
-            || !WPACKET_close(pkt)) {
+           /* Sub-packet ALPN extension */
+        || !WPACKET_start_sub_packet_u16(pkt)
+        || !WPACKET_sub_memcpy_u16(pkt, aval, alen)
+        || !WPACKET_close(pkt)) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return EXT_RETURN_FAIL;
     }
     s->s3.alpn_sent = 1;
-
     return EXT_RETURN_SENT;
 }
 
@@ -485,6 +565,9 @@ EXT_RETURN tls_construct_ctos_use_srtp(SSL_CONNECTION *s, WPACKET *pkt,
 
     if (clnt == NULL)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_use_srtp)
                /* Sub-packet for SRTP extension */
@@ -523,6 +606,9 @@ EXT_RETURN tls_construct_ctos_etm(SSL_CONNECTION *s, WPACKET *pkt,
 {
     if (s->options & SSL_OP_NO_ENCRYPT_THEN_MAC)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_encrypt_then_mac)
             || !WPACKET_put_bytes_u16(pkt, 0)) {
@@ -544,6 +630,9 @@ EXT_RETURN tls_construct_ctos_sct(SSL_CONNECTION *s, WPACKET *pkt,
     /* Not defined for client Certificates */
     if (x != NULL)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_signed_certificate_timestamp)
             || !WPACKET_put_bytes_u16(pkt, 0)) {
@@ -561,6 +650,9 @@ EXT_RETURN tls_construct_ctos_ems(SSL_CONNECTION *s, WPACKET *pkt,
 {
     if (s->options & SSL_OP_NO_EXTENDED_MASTER_SECRET)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_extended_master_secret)
             || !WPACKET_put_bytes_u16(pkt, 0)) {
@@ -589,6 +681,9 @@ EXT_RETURN tls_construct_ctos_supported_versions(SSL_CONNECTION *s, WPACKET *pkt
      */
     if (max_version < TLS1_3_VERSION)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_supported_versions)
             || !WPACKET_start_sub_packet_u16(pkt)
@@ -621,6 +716,10 @@ EXT_RETURN tls_construct_ctos_psk_kex_modes(SSL_CONNECTION *s, WPACKET *pkt,
 #ifndef OPENSSL_NO_TLS1_3
     int nodhe = s->options & SSL_OP_ALLOW_NO_DHE_KEX;
 
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
+
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_psk_kex_modes)
             || !WPACKET_start_sub_packet_u16(pkt)
             || !WPACKET_start_sub_packet_u8(pkt)
@@ -692,7 +791,6 @@ static int add_key_share(SSL_CONNECTION *s, WPACKET *pkt, unsigned int group_id,
         s->s3.tmp.num_ks_pkey++;
 
     OPENSSL_free(encoded_pubkey);
-
     return 1;
  err:
     if (key_share_key != s->s3.tmp.ks_pkey[loop_num])
@@ -713,6 +811,10 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt,
     int add_only_one = 0;
     size_t valid_keyshare = 0;
 
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
+
     /* key_share extension */
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_key_share)
             /* Extension data sub-packet */
@@ -779,6 +881,14 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt,
         return EXT_RETURN_FAIL;
     }
 
+# ifndef OPENSSL_NO_ECH
+    /* stash inner key shares */
+    if (s->ext.ech.ch_depth == 1 && ossl_ech_stash_keyshares(s) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return EXT_RETURN_FAIL;
+    }
+# endif
+
     if (!WPACKET_close(pkt) || !WPACKET_close(pkt)) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return EXT_RETURN_FAIL;
@@ -798,6 +908,9 @@ EXT_RETURN tls_construct_ctos_cookie(SSL_CONNECTION *s, WPACKET *pkt,
     /* Should only be set if we've had an HRR */
     if (s->ext.tls13_cookie_len == 0)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_cookie)
                /* Extension data sub-packet */
@@ -832,6 +945,33 @@ EXT_RETURN tls_construct_ctos_early_data(SSL_CONNECTION *s, WPACKET *pkt,
     const EVP_MD *handmd = NULL;
     SSL *ussl = SSL_CONNECTION_GET_USER_SSL(s);
 
+#ifndef OPENSSL_NO_ECH
+    /*
+     * If we're attempting ECH and processing the outer CH
+     * then we only need to check if the extension is to be
+     * sent or not - any other processing (with side effects)
+     * happened already for the inner CH.
+     */
+    if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) {
+        /*
+         * if we called this for inner and did send then
+         * the following two things should be set, if so,
+         * then send again in the outer CH.
+         */
+        if (s->ext.early_data == SSL_EARLY_DATA_REJECTED
+            && s->ext.early_data_ok == 1) {
+            if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_early_data)
+                || !WPACKET_start_sub_packet_u16(pkt)
+                || !WPACKET_close(pkt)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return EXT_RETURN_FAIL;
+            }
+            return EXT_RETURN_SENT;
+        } else {
+            return EXT_RETURN_NOT_SENT;
+        }
+    }
+#endif
     if (s->hello_retry_request == SSL_HRR_PENDING)
         handmd = ssl_handshake_md(s);
 
@@ -999,6 +1139,9 @@ EXT_RETURN tls_construct_ctos_padding(SSL_CONNECTION *s, WPACKET *pkt,
 
     if ((s->options & SSL_OP_TLSEXT_PADDING) == 0)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt);
+#endif
 
     /*
      * Add padding to workaround bugs in F5 terminators. See RFC7685.
@@ -1120,6 +1263,19 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
             goto dopsksess;
         }
 
+#ifndef OPENSSL_NO_ECH
+        /*
+         * When doing ECH, we get here twice (for inner then outer). The
+         * 2nd time (for outer) we can skip some checks as we know how
+         * those went last time.
+         */
+        if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) {
+            s->ext.tick_identity = s->ext.ech.tick_identity;
+            dores = (s->ext.tick_identity > 0);
+            goto dopsksess;
+        }
+#endif
+
         /*
          * Technically the C standard just says time() returns a time_t and says
          * nothing about the encoding of that type. In practice most
@@ -1130,6 +1286,7 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
          */
         t = ossl_time_subtract(ossl_time_now(), s->session->time);
         agesec = (uint32_t)ossl_time2seconds(t);
+
         /*
          * We calculate the age in seconds but the server may work in ms. Due to
          * rounding errors we could overestimate the age by up to 1s. It is
@@ -1170,6 +1327,11 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
         if (reshashsize <= 0)
             goto dopsksess;
         s->ext.tick_identity++;
+#ifndef OPENSSL_NO_ECH
+        /* stash this for re-use in outer CH */
+        if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 1)
+            s->ext.ech.tick_identity = s->ext.tick_identity;
+#endif
         dores = 1;
     }
 
@@ -1212,6 +1374,77 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
         return EXT_RETURN_FAIL;
     }
 
+# ifndef OPENSSL_NO_ECH
+    /*
+     * For ECH if we're processing the outer CH and the inner CH
+     * has a PSK, then we want to send a GREASE PSK in the outer.
+     * We'll do that by just replacing the ticket value itself
+     * 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;
+
+        if (s->session == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_FAIL;
+        }
+        totalrndsize = s->session->ext.ticklen
+            + sizeof(agems)
+            + s->psksession_id_len
+            + reshashsize
+            + pskhashsize;
+        rndbuf = OPENSSL_malloc(totalrndsize);
+        if (rndbuf == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_FAIL;
+        }
+        /* for outer CH allocate a similar sized random value */
+        if (RAND_bytes_ex(sctx->libctx, rndbuf, totalrndsize, 0) <= 0) {
+            OPENSSL_free(rndbuf);
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_FAIL;
+        }
+        /* set agems from random buffer */
+        rndbufp = rndbuf;
+        agems = *((uint32_t *)(rndbufp));
+        rndbufp += sizeof(agems);
+        if (dores != 0) {
+            if (!WPACKET_sub_memcpy_u16(pkt, rndbufp,
+                                        s->session->ext.ticklen)
+                || !WPACKET_put_bytes_u32(pkt, agems)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                OPENSSL_free(rndbuf);
+                return EXT_RETURN_FAIL;
+            }
+            rndbufp += s->session->ext.ticklen;
+        }
+        if (s->psksession != NULL) {
+            if (!WPACKET_sub_memcpy_u16(pkt, rndbufp, s->psksession_id_len)
+                || !WPACKET_put_bytes_u32(pkt, 0)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                OPENSSL_free(rndbuf);
+                return EXT_RETURN_FAIL;
+            }
+            rndbufp += s->psksession_id_len;
+        }
+        if (!WPACKET_close(pkt)
+            || !WPACKET_start_sub_packet_u16(pkt)
+            || (dores == 1
+                && !WPACKET_sub_memcpy_u8(pkt, rndbufp, reshashsize))
+            || (s->psksession != NULL
+                && !WPACKET_sub_memcpy_u8(pkt, rndbufp, pskhashsize))
+            || !WPACKET_close(pkt)
+            || !WPACKET_close(pkt)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            OPENSSL_free(rndbuf);
+            return EXT_RETURN_FAIL;
+        }
+        OPENSSL_free(rndbuf);
+        return EXT_RETURN_SENT;
+    }
+# endif /* OPENSSL_NO_ECH */
     if (dores) {
         if (!WPACKET_sub_memcpy_u16(pkt, s->session->ext.tick,
                                            s->session->ext.ticklen)
@@ -1280,6 +1513,9 @@ EXT_RETURN tls_construct_ctos_post_handshake_auth(SSL_CONNECTION *s, WPACKET *pk
 #ifndef OPENSSL_NO_TLS1_3
     if (!s->pha_enabled)
         return EXT_RETURN_NOT_SENT;
+# ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(s, context, pkt)
+# endif
 
     /* construct extension - 0 length, no contents */
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_post_handshake_auth)
@@ -1399,28 +1635,32 @@ int tls_parse_stoc_server_name(SSL_CONNECTION *s, PACKET *pkt,
                                unsigned int context,
                                X509 *x, size_t chainidx)
 {
-    if (s->ext.hostname == NULL) {
+    char *eff_sni = s->ext.hostname;
+
+#ifndef OPENSSL_NO_ECH
+    /* if we tried ECH and failed, the outer is what's expected */
+    if (s->ext.ech.es != NULL && s->ext.ech.success == 0)
+        eff_sni = s->ext.ech.outer_hostname;
+#endif
+    if (eff_sni == NULL) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return 0;
     }
-
     if (PACKET_remaining(pkt) > 0) {
         SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
         return 0;
     }
-
     if (!s->hit) {
         if (s->session->ext.hostname != NULL) {
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return 0;
         }
-        s->session->ext.hostname = OPENSSL_strdup(s->ext.hostname);
+        s->session->ext.hostname = OPENSSL_strdup(eff_sni);
         if (s->session->ext.hostname == NULL) {
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return 0;
         }
     }
-
     return 1;
 }
 
@@ -2170,6 +2410,9 @@ EXT_RETURN tls_construct_ctos_client_cert_type(SSL_CONNECTION *sc, WPACKET *pkt,
     sc->ext.client_cert_type_ctos = OSSL_CERT_TYPE_CTOS_NONE;
     if (sc->client_cert_type == NULL)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(sc, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_client_cert_type)
             || !WPACKET_start_sub_packet_u16(pkt)
@@ -2222,6 +2465,9 @@ EXT_RETURN tls_construct_ctos_server_cert_type(SSL_CONNECTION *sc, WPACKET *pkt,
     sc->ext.server_cert_type_ctos = OSSL_CERT_TYPE_CTOS_NONE;
     if (sc->server_cert_type == NULL)
         return EXT_RETURN_NOT_SENT;
+#ifndef OPENSSL_NO_ECH
+    ECH_SAME_EXT(sc, context, pkt)
+#endif
 
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_cert_type)
             || !WPACKET_start_sub_packet_u16(pkt)
@@ -2266,3 +2512,240 @@ int tls_parse_stoc_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt,
     sc->ext.server_cert_type = type;
     return 1;
 }
+
+#ifndef OPENSSL_NO_ECH
+EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
+                                  unsigned int context, X509 *x,
+                                  size_t chainidx)
+{
+    int rv = 0, hpke_mode = OSSL_HPKE_MODE_BASE;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+    unsigned char config_id_to_use = 0x00, info[OSSL_ECH_MAX_INFO_LEN];
+    unsigned char *encoded = NULL, *mypub = NULL;
+    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));
+
+    /* 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)
+        return EXT_RETURN_NOT_SENT;
+    /* send grease if not really attempting ECH */
+    if (grease_opt_set == 1) {
+        if (s->hello_retry_request == SSL_HRR_PENDING
+            && s->ext.ech.sent != NULL) {
+            /* re-tx already sent GREASEy ECH */
+            if (WPACKET_memcpy(pkt, s->ext.ech.sent,
+                               s->ext.ech.sent_len) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return EXT_RETURN_FAIL;
+            }
+            return EXT_RETURN_SENT;
+        }
+        /* if nobody set a type, use the default */
+        if (s->ext.ech.attempted_type == OSSL_ECH_type_unknown)
+            s->ext.ech.attempted_type = TLSEXT_TYPE_ech;
+        if (ossl_ech_send_grease(s, pkt) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_NOT_SENT;
+        }
+        return EXT_RETURN_SENT;
+    }
+
+    /* For the inner CH - we simply include one of these saying "inner" */
+    if (s->ext.ech.ch_depth == 1) {
+        if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech)
+            || !WPACKET_start_sub_packet_u16(pkt)
+            || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_INNER_CH_TYPE)
+            || !WPACKET_close(pkt)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_FAIL;
+        }
+        return EXT_RETURN_SENT;
+    }
+
+    /*
+     * If not GREASEing we prepare sending the outer value - after the
+     * entire thing has been constructed, putting in zeros for now where
+     * we'd otherwise include ECH ciphertext, we later encode and encrypt.
+     * We need to do it that way as we need the rest of the outer CH to
+     * be known and used as AAD input before we do encryption.
+     */
+    if (s->ext.ech.ch_depth != 0)
+        return EXT_RETURN_NOT_SENT;
+    /* Make ClientHelloInner and EncodedClientHelloInner as per spec. */
+    if (ossl_ech_encode_inner(s, &encoded, &encoded_len) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    s->ext.ech.encoded_inner = encoded;
+    s->ext.ech.encoded_inner_len = encoded_len;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("encoded inner CH", encoded, encoded_len);
+# endif
+    rv = ossl_ech_pick_matching_cfg(s, &ee, &hpke_suite);
+    if (rv != 1 || ee == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    s->ext.ech.attempted_type = ee->version;
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "EAAE: selected: version: %4x, config %2x\n",
+                   ee->version, ee->config_id);
+    } 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;
+        }
+# ifdef OSSL_ECH_SUPERVERBOSE
+        ossl_ech_pbuf("EAAE: random config_id", &config_id_to_use, 1);
+# endif
+    }
+    s->ext.ech.attempted_cid = config_id_to_use;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EAAE: peer pub", ee->pub, ee->pub_len);
+    ossl_ech_pbuf("EAAE: clear", encoded, encoded_len);
+    ossl_ech_pbuf("EAAE: ECHConfig", ee->encoded, ee->encoded_len);
+# endif
+    /*
+     * The AAD is the full outer client hello but with the correct number of
+     * zeros for where the ECH ciphertext octets will later be placed. So we
+     * add the ECH extension to the |pkt| but with zeros for ciphertext, that
+     * forms up the AAD, then after we've encrypted, we'll splice in the actual
+     * ciphertext.
+     * Watch out for the "4" offsets that remove the type and 3-octet length
+     * from the encoded CH as per the spec.
+     */
+    clear_len = ossl_ech_calc_padding(s, ee, encoded_len);
+    if (clear_len == 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    lenclen = OSSL_HPKE_get_public_encap_size(hpke_suite);
+    if (s->ext.ech.hpke_ctx == NULL) { /* 1st CH */
+        if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len,
+                                   info, &info_len) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+# ifdef OSSL_ECH_SUPERVERBOSE
+        ossl_ech_pbuf("EAAE info", info, info_len);
+# endif
+        s->ext.ech.hpke_ctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite,
+                                                OSSL_HPKE_ROLE_SENDER,
+                                                sctx->libctx, sctx->propq);
+        if (s->ext.ech.hpke_ctx == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        mypub = OPENSSL_malloc(lenclen);
+        if (mypub == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        mypub_len = lenclen;
+        rv = OSSL_HPKE_encap(s->ext.ech.hpke_ctx, mypub, &mypub_len,
+                             ee->pub, ee->pub_len, info, info_len);
+        if (rv != 1) {
+            OPENSSL_free(mypub);
+            mypub = NULL;
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+        s->ext.ech.pub = mypub;
+        s->ext.ech.pub_len = mypub_len;
+    } else { /* HRR - retrieve public */
+        mypub = s->ext.ech.pub;
+        mypub_len = s->ext.ech.pub_len;
+        if (mypub == NULL || mypub_len == 0) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            goto err;
+        }
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("EAAE: mypub", mypub, mypub_len);
+    WPACKET_get_total_written(pkt, &aad_len); /* use aad_len for tracing */
+    ossl_ech_pbuf("EAAE pkt b4", WPACKET_get_curr(pkt) - aad_len, aad_len);
+# endif
+    cipherlen = OSSL_HPKE_get_ciphertext_size(hpke_suite, clear_len);
+    if (cipherlen <= clear_len || cipherlen > OSSL_ECH_MAX_PAYLOAD_LEN) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        goto err;
+    }
+    s->ext.ech.clearlen = clear_len;
+    s->ext.ech.cipherlen = cipherlen;
+    if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech)
+        || !WPACKET_start_sub_packet_u16(pkt)
+        || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE)
+        || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id)
+        || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id)
+        || !WPACKET_put_bytes_u8(pkt, config_id_to_use)
+        || (s->hello_retry_request == SSL_HRR_PENDING
+            && !WPACKET_put_bytes_u16(pkt, 0x00)) /* no pub */
+        || (s->hello_retry_request != SSL_HRR_PENDING
+            && !WPACKET_sub_memcpy_u16(pkt, mypub, mypub_len))
+        || !WPACKET_start_sub_packet_u16(pkt)
+        || !WPACKET_get_total_written(pkt, &s->ext.ech.cipher_offset)
+        || !WPACKET_memset(pkt, 0, cipherlen)
+        || !WPACKET_close(pkt)
+        || !WPACKET_close(pkt)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* don't count the type + 3-octet length */
+    s->ext.ech.cipher_offset -= 4;
+    return EXT_RETURN_SENT;
+err:
+    return EXT_RETURN_FAIL;
+}
+
+/* if the server thinks we GREASE'd then we may get an ECHConfigList */
+int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
+                       X509 *x, size_t chainidx)
+{
+    size_t rlen = 0;
+    const unsigned char *rval = NULL;
+    unsigned char *srval = NULL;
+    PACKET rcfgs_pkt;
+
+    /*
+     * An HRR will have an ECH extension with the 8-octet confirmation value.
+     * Store it away for when we check it later
+     */
+    if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) {
+        if (PACKET_remaining(pkt) != OSSL_ECH_SIGNAL_LEN) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH);
+            return 0;
+        }
+        s->ext.ech.hrrsignal_p = (unsigned char *)PACKET_data(pkt);
+        memcpy(s->ext.ech.hrrsignal, s->ext.ech.hrrsignal_p,
+               OSSL_ECH_SIGNAL_LEN);
+        return 1;
+    }
+    /* otherwise we expect retry-configs */
+    if (!PACKET_get_length_prefixed_2(pkt, &rcfgs_pkt)) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH);
+        return 0;
+    }
+    rval = PACKET_data(&rcfgs_pkt);
+    rlen = (unsigned int)PACKET_remaining(&rcfgs_pkt);
+    OPENSSL_free(s->ext.ech.returned);
+    s->ext.ech.returned = NULL;
+    srval = OPENSSL_malloc(rlen + 2);
+    if (srval == NULL) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    srval[0] = (rlen >> 8) & 0xff;
+    srval[1] = rlen & 0xff;
+    memcpy(srval + 2, rval, rlen);
+    s->ext.ech.returned = srval;
+    s->ext.ech.returned_len = rlen + 2;
+    return 1;
+}
+#endif /* END_OPENSSL_NO_ECH */
diff --git a/ssl/statem/extensions_cust.c b/ssl/statem/extensions_cust.c
index aa352529c4cc7..5ea06eb0a7271 100644
--- a/ssl/statem/extensions_cust.c
+++ b/ssl/statem/extensions_cust.c
@@ -200,6 +200,65 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x,
             if (!(meth->ext_flags & SSL_EXT_FLAG_RECEIVED))
                 continue;
         }
+
+#ifndef OPENSSL_NO_ECH
+        if ((context & SSL_EXT_CLIENT_HELLO) != 0
+            && s->ext.ech.attempted == 1) {
+            if (s->ext.ech.ch_depth == 1) {
+                /* mark custom CH ext for ECH compression, if doing ECH */
+                if (s->ext.ech.n_outer_only >= OSSL_ECH_OUTERS_MAX) {
+                    OSSL_TRACE_BEGIN(TLS) {
+                        BIO_printf(trc_out,
+                                   "Too many outers to compress (max=%d)\n",
+                                   OSSL_ECH_OUTERS_MAX);
+                    } OSSL_TRACE_END(TLS);
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION);
+                    return 0;
+                }
+                s->ext.ech.outer_only[s->ext.ech.n_outer_only] = meth->ext_type;
+                s->ext.ech.n_outer_only++;
+                OSSL_TRACE_BEGIN(TLS) {
+                    BIO_printf(trc_out, "ECH compressing type "
+                               "0x%04x (tot: %d)\n",
+                               (int) meth->ext_type,
+                               (int) s->ext.ech.n_outer_only);
+                } OSSL_TRACE_END(TLS);
+            }
+            if (s->ext.ech.ch_depth == 0) {
+                /* TODO(ECH): we need a better way to handle indexing exts */
+                /* copy over the extension octets (if any) to outer */
+                int j, tind = -1;
+                RAW_EXTENSION *raws = NULL;
+
+                /* we gotta find the relevant index to copy over this ext */
+                if (s->clienthello == NULL
+                    || s->clienthello->pre_proc_exts == NULL) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION);
+                    return 0;
+                }
+                raws = s->clienthello->pre_proc_exts;
+                for (j = 0; j != (int) s->clienthello->pre_proc_exts_len; j++) {
+                    if (raws[j].type == meth->ext_type) {
+                        tind = j;
+                        break;
+                    }
+                }
+                if (tind == -1) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION);
+                    return 0;
+                }
+                if (ossl_ech_copy_inner2outer(s, meth->ext_type, tind,
+                                              pkt) != OSSL_ECH_SAME_EXT_DONE) {
+                    /* for custom exts, we really should have found it */
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION);
+                    return 0;
+                }
+                /* we're done with that one now */
+                continue;
+            }
+        }
+#endif
+
         /*
          * We skip it if the callback is absent - except for a ClientHello where
          * we add an empty extension.
@@ -600,6 +659,10 @@ int SSL_extension_supported(unsigned int ext_type)
     case TLSEXT_TYPE_compress_certificate:
     case TLSEXT_TYPE_client_cert_type:
     case TLSEXT_TYPE_server_cert_type:
+#ifndef OPENSSL_NO_ECH
+    case TLSEXT_TYPE_ech:
+    case TLSEXT_TYPE_outer_extensions:
+#endif
         return 1;
     default:
         return 0;
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index ac2bddde3b0c7..6a65b8b0b6de6 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -13,7 +13,12 @@
 #include "internal/cryptlib.h"
 #include "internal/ssl_unwrap.h"
 
-#define COOKIE_STATE_FORMAT_VERSION     1
+#ifndef OPENSSL_NO_ECH
+# include 
+# include 
+#endif
+
+#define COOKIE_STATE_FORMAT_VERSION 1
 
 /*
  * 2 bytes for packet length, 2 bytes for format version, 2 bytes for
@@ -1517,7 +1522,7 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
     if (sess == NULL)
         return 1;
 
-    binderoffset = PACKET_data(pkt) - (const unsigned char *)s->init_buf->data;
+    binderoffset = PACKET_data(pkt) - PACKET_msg_start(pkt);
     hashsize = EVP_MD_get_size(md);
     if (hashsize <= 0)
         goto err;
@@ -1538,9 +1543,8 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
         SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
         goto err;
     }
-    if (tls_psk_do_binder(s, md, (const unsigned char *)s->init_buf->data,
-                          binderoffset, PACKET_data(&binder), NULL, sess, 0,
-                          ext) != 1) {
+    if (tls_psk_do_binder(s, md, PACKET_msg_start(pkt), binderoffset,
+                          PACKET_data(&binder), NULL, sess, 0, ext) != 1) {
         /* SSLfatal() already called */
         goto err;
     }
@@ -2428,3 +2432,153 @@ int tls_parse_ctos_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt,
     SSLfatal(sc, SSL_AD_UNSUPPORTED_CERTIFICATE, SSL_R_BAD_EXTENSION);
     return 0;
 }
+
+#ifndef OPENSSL_NO_ECH
+/*
+ * ECH handling for edge cases (GREASE/inner) and errors.
+ * return 1 for good, 0 otherwise
+ *
+ * Real ECH handling (i.e. decryption) happens before, via
+ * ech_early_decrypt(), but if that failed (e.g. decryption
+ * failed, which may be down to GREASE) then we end up here,
+ * processing the ECH from the outer CH.
+ * Otherwise, we only expect to see an inner ECH with a fixed
+ * value here.
+ */
+int tls_parse_ctos_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
+                       X509 *x, size_t chainidx)
+{
+    unsigned int echtype = 0;
+
+    if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) {
+        /* GREASE is fine */
+        return 1;
+    }
+    if (s->ext.ech.es == NULL) {
+        /* If not configured for ECH then we ignore it */
+        return 1;
+    }
+    if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech) {
+        /* if/when new versions of ECH are added we'll update here */
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    /*
+     * we only allow "inner" which is one octet, valued 0x01
+     * and only if we decrypted ok or are a backend
+     */
+    if (PACKET_get_1(pkt, &echtype) != 1
+        || echtype != OSSL_ECH_INNER_CH_TYPE
+        || PACKET_remaining(pkt) != 0) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    if (s->ext.ech.success != 1 && s->ext.ech.backend != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+        return 0;
+    }
+    /* yay - we're ok with this */
+    OSSL_TRACE_BEGIN(TLS) {
+        BIO_printf(trc_out, "ECH seen in inner as expected.\n");
+    } OSSL_TRACE_END(TLS);
+    return 1;
+}
+
+/*
+ * Answer an ECH, as needed
+ * return 1 for good, 0 otherwise
+ *
+ * Return most-recent ECH config for retry, as needed.
+ * If doing HRR we include the confirmation value, but
+ * for now, we'll just add the zeros - the real octets
+ * will be added later via ech_calc_ech_confirm() which
+ * is called when constructing the server hello.
+ */
+EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt,
+                                  unsigned int context, X509 *x,
+                                  size_t chainidx)
+{
+    unsigned char *rcfgs = NULL;
+    size_t rcfgslen = 0;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+
+    if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST
+        && (s->ext.ech.success == 1 || s->ext.ech.backend == 1)
+        && s->ext.ech.attempted_type == TLSEXT_TYPE_ech) {
+        unsigned char eightzeros[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+
+        if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type)
+            || !WPACKET_sub_memcpy_u16(pkt, eightzeros, 8)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "set 8 zeros for ECH accept confirm in HRR\n");
+        } OSSL_TRACE_END(TLS);
+        return EXT_RETURN_SENT;
+    }
+    /* GREASE or error => random confirmation in HRR case */
+    if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST
+        && s->ext.ech.attempted_type == TLSEXT_TYPE_ech
+        && s->ext.ech.attempted == 1) {
+        unsigned char randomconf[8];
+
+        if (RAND_bytes_ex(sctx->libctx, randomconf, 8,
+                          RAND_DRBG_STRENGTH) <= 0) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type)
+            || !WPACKET_sub_memcpy_u16(pkt, randomconf, 8)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return 0;
+        }
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "set random for ECH acccpt confirm in HRR\n");
+        } OSSL_TRACE_END(TLS);
+        return EXT_RETURN_SENT;
+    }
+    /* in other HRR circumstances: don't set */
+    if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST)
+        return EXT_RETURN_NOT_SENT;
+    /* If in some weird state we ignore and send nothing */
+    if (s->ext.ech.grease != OSSL_ECH_IS_GREASE
+        || s->ext.ech.attempted_type != TLSEXT_TYPE_ech)
+        return EXT_RETURN_NOT_SENT;
+    /*
+     * If the client GREASEd, or we think it did, return the
+     * most-recently loaded ECHConfigList, as the value of the
+     * extension. Most-recently loaded can be anywhere in the
+     * list, depending on changing or non-changing file names.
+     */
+    if (s->ext.ech.es == NULL) {
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "ECH - not sending ECHConfigList to client "
+                       "even though they GREASE'd as I've no loaded configs\n");
+        } OSSL_TRACE_END(TLS);
+        return EXT_RETURN_NOT_SENT;
+    }
+    if (ossl_ech_get_retry_configs(s, &rcfgs, &rcfgslen) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        return 0;
+    }
+    if (rcfgslen == 0) {
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "ECH - not sending ECHConfigList to client "
+                       "even though they GREASE'd and I have configs but "
+                       "I've no configs set to be returned\n");
+        } OSSL_TRACE_END(TLS);
+        return EXT_RETURN_NOT_SENT;
+    }
+    if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech)
+        || !WPACKET_start_sub_packet_u16(pkt)
+        || !WPACKET_sub_memcpy_u16(pkt, rcfgs, rcfgslen)
+        || !WPACKET_close(pkt)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        OPENSSL_free(rcfgs);
+        return 0;
+    }
+    OPENSSL_free(rcfgs);
+    return EXT_RETURN_SENT;
+}
+#endif /* END OPENSSL_NO_ECH */
diff --git a/ssl/statem/statem.c b/ssl/statem/statem.c
index 864a2f1a339f0..97df583f5dcde 100644
--- a/ssl/statem/statem.c
+++ b/ssl/statem/statem.c
@@ -588,7 +588,7 @@ static SUB_STATE_RETURN read_state_machine(SSL_CONNECTION *s)
 {
     OSSL_STATEM *st = &s->statem;
     int ret, mt;
-    size_t len = 0;
+    size_t len = 0, headerlen;
     int (*transition) (SSL_CONNECTION *s, int mt);
     PACKET pkt;
     MSG_PROCESS_RETURN(*process_message) (SSL_CONNECTION *s, PACKET *pkt);
@@ -682,10 +682,23 @@ static SUB_STATE_RETURN read_state_machine(SSL_CONNECTION *s)
             }
 
             s->first_packet = 0;
-            if (!PACKET_buf_init(&pkt, s->init_msg, len)) {
+            /*
+             * We initialise the buffer including the message header, and
+             * then skip over header ready to process the message. This
+             * ensures that calls to PACKET_msg_start() gives us the whole
+             * message
+             */
+            headerlen = (char *)s->init_msg - s->init_buf->data;
+            if (!PACKET_buf_init(&pkt, (unsigned char *)s->init_buf->data,
+                                 len + headerlen)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return SUB_STATE_ERROR;
+            }
+            if (!PACKET_forward(&pkt, headerlen)) {
                 SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
                 return SUB_STATE_ERROR;
             }
+
             ret = process_message(s, &pkt);
 
             /* Discard the packet data */
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index 5639048a8e6be..18f4673d1ec09 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -32,14 +32,12 @@
 #include 
 
 static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s,
-                                                             PACKET *pkt);
+                                                             RAW_EXTENSION *extensions);
 static MSG_PROCESS_RETURN tls_process_encrypted_extensions(SSL_CONNECTION *s,
                                                            PACKET *pkt);
 
 static ossl_inline int cert_req_allowed(SSL_CONNECTION *s);
 static int key_exchange_expected(SSL_CONNECTION *s);
-static int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
-                                    WPACKET *pkt);
 
 static ossl_inline int received_server_cert(SSL_CONNECTION *sc)
 {
@@ -1175,7 +1173,232 @@ WORK_STATE ossl_statem_client_post_process_message(SSL_CONNECTION *s,
     }
 }
 
-CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt)
+#ifndef OPENSSL_NO_ECH
+/*
+ * Wrap ClientHello construction with ECH code.
+ *
+ * As needed, we'll call the CH constructor twice, first for
+ * inner, and then for outer.
+ *
+ * `tls_construct_client_hello_aux` is the pre-ECH code
+ * and the ECH-aware tls_construct_client_hello just calls
+ * that if there's no ECH involved, but otherwise does ECH
+ * things around calls to the _aux variant.
+ *
+ * Our basic model is that, when really attempting ECH we
+ * indicate via the ch_depth field whether we're dealing
+ * with inner or outer CH (1 for inner, 0 for outer).
+ *
+ * After creating the fields for the inner CH, we encode
+ * those (so we can re-use existing code) then decode again
+ * (using the existing tls_process_client_hello previously
+ * only used on servers), again to maximise code re-use.
+ *
+ * We next re-encode inner but this time including the
+ * optimisations for inner CH "compression" (outer exts etc.)
+ * to produce our plaintext for encrypting.
+ *
+ * We then process the outer CH in more or less the
+ * usual manner.
+ *
+ * We lastly form up the AAD etc and encrypt to give us
+ * the ciphertext for inclusion in the value of the outer
+ * CH ECH extension.
+ *
+ * It may seem odd to form up the outer CH before
+ * encrypting, but we need to do it that way so we get
+ * the octets for the AAD used in encryption.
+ *
+ * Phew!
+ */
+static int tls_construct_client_hello_aux(SSL_CONNECTION *s, WPACKET *pkt);
+
+__owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s,
+                                                  WPACKET *pkt)
+{
+    WPACKET inner; /* "fake" pkt for inner */
+    BUF_MEM *inner_mem = NULL;
+    PACKET rpkt; /* we'll decode back the inner ch to help make the outer */
+    SSL_SESSION *sess = NULL;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
+    size_t sess_id_len = 0, innerlen = 0;
+    int mt = SSL3_MT_CLIENT_HELLO, rv = 0;
+    OSSL_HPKE_SUITE suite;
+    OSSL_ECHSTORE_ENTRY *ee = NULL;
+    /* Work out what SSL/TLS/DTLS version to use */
+    int protverr = ssl_set_client_hello_version(s);
+
+    if (protverr != 0) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr);
+        return 0;
+    }
+    /* If we're not really attempting ECH, just call existing code.  */
+    if (s->ext.ech.es == NULL)
+        return tls_construct_client_hello_aux(s, pkt);
+    /* note version we're attempting and that an attempt is being made */
+    if (s->ext.ech.es->entries != NULL) {
+        if (ossl_ech_pick_matching_cfg(s, &ee, &suite) != 1 || ee == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_UNSUPPORTED);
+            return 0;
+        }
+        if (ee->version != OSSL_ECH_RFCXXXX_VERSION) {
+            /* we only support that version for now */
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_UNSUPPORTED);
+            return 0;
+        }
+        s->ext.ech.attempted_type = TLSEXT_TYPE_ech;
+        s->ext.ech.attempted_cid = ee->config_id;
+        s->ext.ech.attempted = 1;
+        if (s->ext.ech.outer_hostname == NULL && ee->public_name != NULL) {
+            s->ext.ech.outer_hostname = OPENSSL_strdup((char *)ee->public_name);
+            if (s->ext.ech.outer_hostname == NULL) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return 0;
+            }
+        }
+    }
+    /* If doing real ECH and application requested GREASE too, over-ride that */
+    if (s->ext.ech.grease == OSSL_ECH_IS_GREASE && s->ext.ech.attempted == 1)
+        s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
+    /*
+     * Session ID is handled "oddly" by not being encoded into inner CH (an
+     * optimisation) so is the same for both inner and outer.
+     */
+    sess = s->session;
+    if (sess == NULL
+        || !ssl_version_supported(s, sess->ssl_version, NULL)
+        || !SSL_SESSION_is_resumable(sess)) {
+        if (s->hello_retry_request == SSL_HRR_NONE
+            && !ssl_get_new_session(s, 0))
+            return 0; /* SSLfatal() already called */
+    }
+    if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) {
+        if (s->version == TLS1_3_VERSION
+            && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) {
+            sess_id_len = sizeof(s->tmp_session_id);
+            s->tmp_session_id_len = sess_id_len;
+            if (s->hello_retry_request == SSL_HRR_NONE
+                && RAND_bytes_ex(sctx->libctx, s->tmp_session_id,
+                                 sess_id_len, 0) <= 0) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return 0;
+            }
+            memcpy(s->session->session_id, s->tmp_session_id, sess_id_len);
+            s->session->session_id_length = sess_id_len;
+        } else {
+            sess_id_len = 0;
+        }
+    } else {
+        assert(s->session->session_id_length <= sizeof(s->session->session_id));
+        sess_id_len = s->session->session_id_length;
+        if (s->version == TLS1_3_VERSION) {
+            s->tmp_session_id_len = sess_id_len;
+            memcpy(s->tmp_session_id, s->session->session_id, sess_id_len);
+        }
+    }
+    if (s->hello_retry_request != SSL_HRR_NONE)
+        s->ext.ech.n_outer_only = 0; /* reset count of "compressed" exts */
+    /*
+     * Set CH depth flag so that other code (e.g. extension handlers)
+     * know where we're at: 1 is "inner CH", 0 is "outer CH"
+     */
+    s->ext.ech.ch_depth = 1;
+    if ((inner_mem = BUF_MEM_new()) == NULL
+        || !WPACKET_init(&inner, inner_mem)
+        || !ssl_set_handshake_header(s, &inner, mt)
+        || tls_construct_client_hello_aux(s, &inner) != 1
+        || !WPACKET_close(&inner)
+        || !WPACKET_get_length(&inner, &innerlen)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    OPENSSL_free(s->ext.ech.innerch);
+    s->ext.ech.innerch = (unsigned char *)inner_mem->data;
+    inner_mem->data = NULL;
+    s->ext.ech.innerch_len = innerlen;
+    /* add inner to transcript */
+    if (ossl_ech_intbuf_add(s, s->ext.ech.innerch, innerlen, 0) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    WPACKET_cleanup(&inner);
+    BUF_MEM_free(inner_mem);
+    inner_mem = NULL;
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("inner CH", s->ext.ech.innerch, s->ext.ech.innerch_len);
+    ossl_ech_pbuf("inner, client_random", s->ext.ech.client_random,
+                  SSL3_RANDOM_SIZE);
+    ossl_ech_pbuf("inner, session_id", s->session->session_id,
+                  s->session->session_id_length);
+# endif
+    /* Decode inner so that we can make up encoded inner */
+    if (!PACKET_buf_init(&rpkt, (unsigned char *)s->ext.ech.innerch + 4,
+                         s->ext.ech.innerch_len - 4)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /*
+     * Parse the full inner CH (usually done on server). This gets us
+     * individually encoded extensions so we can choose to compress
+     * and/or to re-use the same value in outer.
+     */
+    if (!tls_process_client_hello(s, &rpkt)) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+
+    s->ext.ech.ch_depth = 0; /* set depth for outer CH */
+    /*
+     * If we want different key shares for inner and outer, then
+     * zap the one for the inner. The inner key_share is stashed
+     * in s.ext.ech.tmp_pkey already.
+     */
+    if (ossl_ech_same_key_share() == 0) {
+        EVP_PKEY_free(s->s3.tmp.pkey);
+        s->s3.tmp.pkey = NULL;
+    }
+    /* Make second call into CH construction for outer CH. */
+    rv = tls_construct_client_hello_aux(s, pkt);
+    if (rv != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+# ifdef OSSL_ECH_SUPERVERBOSE
+    ossl_ech_pbuf("outer, client_random", s->s3.client_random,
+                  SSL3_RANDOM_SIZE);
+    ossl_ech_pbuf("outer, session_id", s->session->session_id,
+                  s->session->session_id_length);
+# endif
+    /* Finally, calculate AAD and encrypt using HPKE */
+    if (ossl_ech_aad_and_encrypt(s, pkt) != 1) {
+        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+        goto err;
+    }
+    /* Free up raw exts as needed (happens like this on real server) */
+    if (s->clienthello != NULL
+        && s->clienthello->pre_proc_exts != NULL) {
+        OPENSSL_free(s->clienthello->pre_proc_exts);
+        OPENSSL_free(s->clienthello);
+        s->clienthello = NULL;
+    }
+    return 1;
+err:
+    if (inner_mem != NULL) {
+        WPACKET_cleanup(&inner);
+        BUF_MEM_free(inner_mem);
+    }
+    if (s->clienthello != NULL) {
+        OPENSSL_free(s->clienthello->pre_proc_exts);
+        OPENSSL_free(s->clienthello);
+        s->clienthello = NULL;
+    }
+    return 0;
+}
+
+static int tls_construct_client_hello_aux(SSL_CONNECTION *s, WPACKET *pkt)
+#else
+__owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt)
+#endif
 {
     unsigned char *p;
     size_t sess_id_len;
@@ -1194,18 +1417,28 @@ CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt)
         return CON_FUNC_ERROR;
     }
 
-    if (sess == NULL
+#ifndef OPENSSL_NO_ECH
+    /* if we're doing ECH, re-use session ID setup earlier */
+    if (s->ext.ech.es == NULL)
+#endif
+        if (sess == NULL
             || !ssl_version_supported(s, sess->ssl_version, NULL)
             || !SSL_SESSION_is_resumable(sess)) {
-        if (s->hello_retry_request == SSL_HRR_NONE
-                && !ssl_get_new_session(s, 0)) {
-            /* SSLfatal() already called */
-            return CON_FUNC_ERROR;
+            if (s->hello_retry_request == SSL_HRR_NONE
+                    && !ssl_get_new_session(s, 0)) {
+                /* SSLfatal() already called */
+                return CON_FUNC_ERROR;
+            }
         }
-    }
-    /* else use the pre-loaded session */
+        /* else use the pre-loaded session */
 
-    p = s->s3.client_random;
+#ifndef OPENSSL_NO_ECH
+    /* use different client_random fields for inner and outer */
+    if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 1)
+        p = s->ext.ech.client_random;
+    else
+#endif
+        p = s->s3.client_random;
 
     /*
      * for DTLS if client_random is initialized, reuse it, we are
@@ -1264,36 +1497,46 @@ CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt)
      * supported_versions extension for the real supported versions.
      */
     if (!WPACKET_put_bytes_u16(pkt, s->client_version)
-            || !WPACKET_memcpy(pkt, s->s3.client_random, SSL3_RANDOM_SIZE)) {
+            || !WPACKET_memcpy(pkt, p, SSL3_RANDOM_SIZE)) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return CON_FUNC_ERROR;
     }
 
     /* Session ID */
     session_id = s->session->session_id;
-    if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) {
-        if (s->version == TLS1_3_VERSION
-                && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) {
-            sess_id_len = sizeof(s->tmp_session_id);
-            s->tmp_session_id_len = sess_id_len;
-            session_id = s->tmp_session_id;
-            if (s->hello_retry_request == SSL_HRR_NONE
-                    && RAND_bytes_ex(sctx->libctx, s->tmp_session_id,
-                                     sess_id_len, 0) <= 0) {
-                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-                return CON_FUNC_ERROR;
+#ifndef OPENSSL_NO_ECH
+    /* same session ID is used for inner/outer when doing ECH */
+    if (s->ext.ech.es != NULL) {
+        sess_id_len = sizeof(s->tmp_session_id);
+    } else {
+#endif
+        if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) {
+            if (s->version == TLS1_3_VERSION
+                    && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) {
+                sess_id_len = sizeof(s->tmp_session_id);
+                s->tmp_session_id_len = sess_id_len;
+                session_id = s->tmp_session_id;
+                if (s->hello_retry_request == SSL_HRR_NONE
+                        && RAND_bytes_ex(sctx->libctx, s->tmp_session_id,
+                                         sess_id_len, 0) <= 0) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    return CON_FUNC_ERROR;
+                }
+            } else {
+                sess_id_len = 0;
             }
         } else {
-            sess_id_len = 0;
-        }
-    } else {
-        assert(s->session->session_id_length <= sizeof(s->session->session_id));
-        sess_id_len = s->session->session_id_length;
-        if (s->version == TLS1_3_VERSION) {
-            s->tmp_session_id_len = sess_id_len;
-            memcpy(s->tmp_session_id, s->session->session_id, sess_id_len);
+            assert(s->session->session_id_length <= sizeof(s->session->session_id));
+            sess_id_len = s->session->session_id_length;
+            if (s->version == TLS1_3_VERSION) {
+                s->tmp_session_id_len = sess_id_len;
+                memcpy(s->tmp_session_id, s->session->session_id, sess_id_len);
+            }
         }
+#ifndef OPENSSL_NO_ECH
     }
+#endif
+
     if (!WPACKET_start_sub_packet_u8(pkt)
             || (sess_id_len != 0 && !WPACKET_memcpy(pkt, session_id,
                                                     sess_id_len))
@@ -1482,6 +1725,24 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
 #ifndef OPENSSL_NO_COMP
     SSL_COMP *comp;
 #endif
+#ifndef OPENSSL_NO_ECH
+    const unsigned char *shbuf = NULL;
+    size_t shlen, alen;
+    /*
+     * client and server accept signal buffers, initialise in case of
+     * e.g. memory fail when calculating, only really applies when
+     * SUPERVERBOSE is defined and we trace these.
+     */
+    unsigned char c_signal[OSSL_ECH_SIGNAL_LEN] = { 0 };
+    unsigned char s_signal[OSSL_ECH_SIGNAL_LEN] = { 0xff };
+    unsigned char *abuf = NULL;
+
+    shlen = PACKET_remaining(pkt);
+    if (PACKET_peek_bytes(pkt, &shbuf, shlen) != 1) {
+        SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH);
+        goto err;
+    }
+#endif
 
     if (!PACKET_get_net_2(pkt, &sversion)) {
         SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH);
@@ -1546,7 +1807,16 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
         goto err;
     }
 
-    if (!hrr) {
+    if (hrr) {
+        if (!tls_collect_extensions(s, &extpkt, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
+                                    &extensions, NULL, 1)
+            || !tls_parse_extension(s, TLSEXT_IDX_ech,
+                SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
+                extensions, NULL, 0)) {
+            /* SSLfatal() already called */
+            goto err;
+        }
+    } else {
         if (!tls_collect_extensions(s, &extpkt,
                                     SSL_EXT_TLS1_2_SERVER_HELLO
                                     | SSL_EXT_TLS1_3_SERVER_HELLO,
@@ -1554,7 +1824,93 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
             /* SSLfatal() already called */
             goto err;
         }
+    }
+
+#ifndef OPENSSL_NO_ECH
+    /*
+     * If we sent an ECH then check if that worked based on the
+     * ServerHello.random confirmation trick. If that is good
+     * then we'll swap over the inner and outer contexts and
+     * proceed with inner. There are some HRR wrinkles too
+     * though.
+     */
+    if (s->ext.ech.es != NULL
+        && s->ext.ech.done != 1 && s->ext.ech.ch_depth == 0
+        && s->ext.ech.grease == OSSL_ECH_NOT_GREASE
+        && s->ext.ech.attempted_type == TLSEXT_TYPE_ech) {
+        if (!set_client_ciphersuite(s, cipherchars)) {
+            /* SSLfatal() already called */
+            goto err;
+        }
+        /* add any SH/HRR to inner transcript if we tried ECH */
+        if (s->ext.ech.attempted == 1) {
+            unsigned char prelude[4];
+
+            prelude[0] = SSL3_MT_SERVER_HELLO;
+            prelude[1] = (shlen >> 16) & 0xff;
+            prelude[2] = (shlen >> 8) & 0xff;
+            prelude[3] = shlen & 0xff;
+            if (ossl_ech_intbuf_add(s, prelude, sizeof(prelude), hrr) != 1
+                || ossl_ech_intbuf_add(s, shbuf, shlen, 0) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        }
+        /* check the ECH accept signal */
+        if (ossl_ech_calc_confirm(s, hrr, c_signal, shlen) != 1) {
+            /* SSLfatal() already called */
+            OSSL_TRACE(TLS, "ECH calc confirm failed\n");
+            goto err;
+        }
+        if (ossl_ech_find_confirm(s, hrr, s_signal) != 1
+            || memcmp(s_signal, c_signal, sizeof(c_signal)) != 0) {
+            OSSL_TRACE(TLS, "ECH accept check failed\n");
+# ifdef OSSL_ECH_SUPERVERBOSE
+            ossl_ech_pbuf("ECH client accept val:", c_signal, sizeof(c_signal));
+            ossl_ech_pbuf("ECH server accept val:", s_signal, sizeof(s_signal));
+# endif
+            s->ext.ech.success = 0;
+        } else { /* match, ECH worked */
+            OSSL_TRACE_BEGIN(TLS) {
+                BIO_printf(trc_out, "ECH accept check ok\n");
+                BIO_printf(trc_out, "ECH set session hostname to %s\n",
+                           s->ext.hostname ? s->ext.hostname : "NULL");
+            } OSSL_TRACE_END(TLS);
+            s->ext.ech.success = 1;
+        }
+        /* we're done with that hrrsignal (if we got one) */
+        s->ext.ech.hrrsignal_p = NULL;
+        if (!hrr && s->ext.ech.success == 1) {
+            if (ossl_ech_swaperoo(s) != 1
+                || ossl_ech_intbuf_fetch(s, &abuf, &alen) != 1
+                || ossl_ech_reset_hs_buffer(s, abuf, alen) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        } else if (!hrr) {
+            /*
+             * If we got retry_configs then we should be validating
+             * the outer CH, so we better set the hostname for the
+             * connection accordingly.
+             */
+            s->ext.ech.former_inner = s->ext.hostname;
+            s->ext.hostname = NULL;
+            if (s->ext.ech.outer_hostname != NULL) {
+                s->ext.hostname = OPENSSL_strdup(s->ext.ech.outer_hostname);
+                if (s->ext.hostname == NULL) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    goto err;
+                }
+                if (SSL_set1_host(ssl, s->ext.ech.outer_hostname) != 1) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    goto err;
+                }
+            }
+        }
+    }
+#endif
 
+    if (!hrr) {
         if (!ssl_choose_client_version(s, sversion, extensions)) {
             /* SSLfatal() already called */
             goto err;
@@ -1577,12 +1933,17 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
     }
 
     if (hrr) {
+        int ret;
+
         if (!set_client_ciphersuite(s, cipherchars)) {
             /* SSLfatal() already called */
             goto err;
         }
 
-        return tls_process_as_hello_retry_request(s, &extpkt);
+        ret = tls_process_as_hello_retry_request(s, extensions);
+        OPENSSL_free(extensions);
+
+        return ret;
     }
 
     /*
@@ -1837,10 +2198,8 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
 }
 
 static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s,
-                                                             PACKET *extpkt)
+                                                             RAW_EXTENSION *extensions)
 {
-    RAW_EXTENSION *extensions = NULL;
-
     /*
      * If we were sending early_data then any alerts should not be sent using
      * the old wrlmethod.
@@ -1858,17 +2217,12 @@ static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s,
     /* We are definitely going to be using TLSv1.3 */
     s->rlayer.wrlmethod->set_protocol_version(s->rlayer.wrl, TLS1_3_VERSION);
 
-    if (!tls_collect_extensions(s, extpkt, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
-                                &extensions, NULL, 1)
-            || !tls_parse_all_extensions(s, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
-                                         extensions, NULL, 0, 1)) {
+    if (!tls_parse_all_extensions(s, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST,
+                                  extensions, NULL, 0, 1)) {
         /* SSLfatal() already called */
         goto err;
     }
 
-    OPENSSL_free(extensions);
-    extensions = NULL;
-
     if (s->ext.tls13_cookie_len == 0 && s->s3.tmp.pkey != NULL) {
         /*
          * We didn't receive a cookie or a new key_share so the next
@@ -1901,7 +2255,6 @@ static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s,
 
     return MSG_PROCESS_FINISHED_READING;
  err:
-    OPENSSL_free(extensions);
     return MSG_PROCESS_ERROR;
 }
 
@@ -3031,6 +3384,22 @@ int tls_process_initial_server_flight(SSL_CONNECTION *s)
     }
 #endif
 
+#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
+        && s->ext.ech.grease != OSSL_ECH_IS_GREASE) {
+        SSLfatal(s, SSL_AD_ECH_REQUIRED, SSL_R_ECH_REQUIRED);
+        return 0;
+    }
+#endif /* OPENSSL_NO_ECH */
+
     return 1;
 }
 
@@ -4138,110 +4507,6 @@ int ssl_do_client_cert_cb(SSL_CONNECTION *s, X509 **px509, EVP_PKEY **ppkey)
     return i;
 }
 
-int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk,
-                             WPACKET *pkt)
-{
-    int i;
-    size_t totlen = 0, len, maxlen, maxverok = 0;
-    int empty_reneg_info_scsv = !s->renegotiate
-                                && !SSL_CONNECTION_IS_DTLS(s)
-                                && ssl_security(s, SSL_SECOP_VERSION, 0, TLS1_VERSION, NULL)
-                                && s->min_proto_version <= TLS1_VERSION;
-    SSL *ssl = SSL_CONNECTION_GET_SSL(s);
-
-    /* Set disabled masks for this session */
-    if (!ssl_set_client_disabled(s)) {
-        SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_PROTOCOLS_AVAILABLE);
-        return 0;
-    }
-
-    if (sk == NULL) {
-        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-        return 0;
-    }
-
-#ifdef OPENSSL_MAX_TLS1_2_CIPHER_LENGTH
-# if OPENSSL_MAX_TLS1_2_CIPHER_LENGTH < 6
-#  error Max cipher length too short
-# endif
-    /*
-     * Some servers hang if client hello > 256 bytes as hack workaround
-     * chop number of supported ciphers to keep it well below this if we
-     * use TLS v1.2
-     */
-    if (TLS1_get_version(ssl) >= TLS1_2_VERSION)
-        maxlen = OPENSSL_MAX_TLS1_2_CIPHER_LENGTH & ~1;
-    else
-#endif
-        /* Maximum length that can be stored in 2 bytes. Length must be even */
-        maxlen = 0xfffe;
-
-    if (empty_reneg_info_scsv)
-        maxlen -= 2;
-    if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV)
-        maxlen -= 2;
-
-    for (i = 0; i < sk_SSL_CIPHER_num(sk) && totlen < maxlen; i++) {
-        const SSL_CIPHER *c;
-
-        c = sk_SSL_CIPHER_value(sk, i);
-        /* Skip disabled ciphers */
-        if (ssl_cipher_disabled(s, c, SSL_SECOP_CIPHER_SUPPORTED, 0))
-            continue;
-
-        if (!ssl->method->put_cipher_by_char(c, pkt, &len)) {
-            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-            return 0;
-        }
-
-        /* Sanity check that the maximum version we offer has ciphers enabled */
-        if (!maxverok) {
-            int minproto = SSL_CONNECTION_IS_DTLS(s) ? c->min_dtls : c->min_tls;
-            int maxproto = SSL_CONNECTION_IS_DTLS(s) ? c->max_dtls : c->max_tls;
-
-            if (ssl_version_cmp(s, maxproto, s->s3.tmp.max_ver) >= 0
-                    && ssl_version_cmp(s, minproto, s->s3.tmp.max_ver) <= 0)
-                maxverok = 1;
-        }
-
-        totlen += len;
-    }
-
-    if (totlen == 0 || !maxverok) {
-        const char *maxvertext =
-            !maxverok
-            ? "No ciphers enabled for max supported SSL/TLS version"
-            : NULL;
-
-        SSLfatal_data(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_CIPHERS_AVAILABLE,
-                      maxvertext);
-        return 0;
-    }
-
-    if (totlen != 0) {
-        if (empty_reneg_info_scsv) {
-            static const SSL_CIPHER scsv = {
-                0, NULL, NULL, SSL3_CK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0
-            };
-            if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) {
-                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-                return 0;
-            }
-        }
-        if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) {
-            static const SSL_CIPHER scsv = {
-                0, NULL, NULL, SSL3_CK_FALLBACK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0
-            };
-            if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) {
-                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-                return 0;
-            }
-        }
-    }
-
-    return 1;
-}
-
 CON_FUNC_RETURN tls_construct_end_of_early_data(SSL_CONNECTION *s, WPACKET *pkt)
 {
     if (s->early_data_state != SSL_EARLY_DATA_WRITE_RETRY
diff --git a/ssl/statem/statem_lib.c b/ssl/statem/statem_lib.c
index 65530df7a433c..82582af54cce8 100644
--- a/ssl/statem/statem_lib.c
+++ b/ssl/statem/statem_lib.c
@@ -2205,6 +2205,18 @@ int ssl_choose_server_version(SSL_CONNECTION *s, CLIENTHELLO_MSG *hello,
 
     suppversions = &hello->pre_proc_exts[TLSEXT_IDX_supported_versions];
 
+#ifndef OPENSSL_NO_ECH
+    /*
+     * Check we're dealing with a TLSv1.3 connection when ECH has
+     * succeeded, and not with a smuggled earlier version ClientHello
+     * (which could be a form of attack).
+     * This bit checks there is a supported version present, a little
+     * bit further below, we check that that version is TLSv1.3
+     */
+    if (!suppversions->present && s->ext.ech.success == 1)
+        return SSL_R_UNSUPPORTED_PROTOCOL;
+#endif
+
     /* If we did an HRR then supported versions is mandatory */
     if (!suppversions->present && s->hello_retry_request != SSL_HRR_NONE)
         return SSL_R_UNSUPPORTED_PROTOCOL;
@@ -2246,6 +2258,11 @@ int ssl_choose_server_version(SSL_CONNECTION *s, CLIENTHELLO_MSG *hello,
         }
 
         if (best_vers > 0) {
+#ifndef OPENSSL_NO_ECH
+            /* ECH needs TLSV1.3 also */
+            if (s->ext.ech.success == 1 && best_vers != TLS1_3_VERSION)
+                return SSL_R_UNSUPPORTED_PROTOCOL;
+#endif
             if (s->hello_retry_request != SSL_HRR_NONE) {
                 /*
                  * This is after a HelloRetryRequest so we better check that we
diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h
index 48870683c3426..8ba5ef242a8b2 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -570,3 +570,15 @@ int tls_parse_ctos_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt,
 int tls_parse_stoc_server_cert_type(SSL_CONNECTION *s, PACKET *pkt,
                                     unsigned int context,
                                     X509 *x, size_t chainidx);
+#ifndef OPENSSL_NO_ECH
+EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
+                                  unsigned int context, X509 *x,
+                                  size_t chainidx);
+int tls_parse_ctos_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
+                       X509 *x, size_t chainidx);
+EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt,
+                                  unsigned int context, X509 *x,
+                                  size_t chainidx);
+int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
+                       X509 *x, size_t chainidx);
+#endif
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index 5b6465bc23960..89944706701e0 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -35,6 +35,10 @@
 
 #define TICKET_NONCE_SIZE       8
 
+#ifndef OPENSSL_NO_ECH
+# include "../ech/ech_local.h"
+#endif
+
 typedef struct {
   ASN1_TYPE *kxBlob;
   ASN1_TYPE *opaqueBlob;
@@ -1495,6 +1499,86 @@ MSG_PROCESS_RETURN tls_process_client_hello(SSL_CONNECTION *s, PACKET *pkt)
     static const unsigned char null_compression = 0;
     CLIENTHELLO_MSG *clienthello = NULL;
 
+#ifndef OPENSSL_NO_ECH
+    /*
+     * For a split-mode backend we want to have a way to point at the CH octets
+     * for the accept-confirmation calculation. The split-mode backend does not
+     * need any ECH secrets, but it does need to see the inner CH and be the TLS
+     * endpoint with which the ECH encrypting client sets up the TLS session.
+     * The split-mode backend however does need to do an ECH confirm calculation
+     * so we need to tee that up. The result of that calculation will be put in
+     * the ServerHello.random (or ECH extension if HRR) to signal to the client
+     * that ECH "worked."
+     */
+    if (s->server && PACKET_remaining(pkt) != 0) {
+        int rv = 0, innerflag = -1;
+        size_t startofsessid = 0, startofexts = 0, echoffset = 0;
+        size_t outersnioffset = 0; /* offset to SNI in outer */
+        uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */
+        const unsigned char *pbuf = NULL;
+
+        /* reset needed in case of HRR */
+        s->ext.ech.ch_offsets_done = 0;
+        rv = ossl_ech_get_ch_offsets(s, pkt, &startofsessid, &startofexts,
+                                     &echoffset, &echtype, &innerflag,
+                                     &outersnioffset);
+        if (rv != 1) {
+            SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION);
+            goto err;
+        }
+        if (innerflag == OSSL_ECH_INNER_CH_TYPE) {
+            WPACKET inner;
+
+            OSSL_TRACE(TLS, "Got inner ECH so setting backend\n");
+            /* For backend, include msg type & 3 octet length */
+            s->ext.ech.backend = 1;
+            s->ext.ech.attempted_type = TLSEXT_TYPE_ech;
+            OPENSSL_free(s->ext.ech.innerch);
+            s->ext.ech.innerch_len = PACKET_remaining(pkt);
+            if (PACKET_peek_bytes(pkt, &pbuf, s->ext.ech.innerch_len) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+            s->ext.ech.innerch_len += SSL3_HM_HEADER_LENGTH; /* 4 */
+            s->ext.ech.innerch = OPENSSL_malloc(s->ext.ech.innerch_len);
+            if (s->ext.ech.innerch == NULL) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+            if (!WPACKET_init_static_len(&inner, s->ext.ech.innerch,
+                                         s->ext.ech.innerch_len, 0)
+                || !WPACKET_put_bytes_u8(&inner, SSL3_MT_CLIENT_HELLO)
+                || !WPACKET_put_bytes_u24(&inner, s->ext.ech.innerch_len
+                                          - SSL3_HM_HEADER_LENGTH)
+                || !WPACKET_memcpy(&inner, pbuf, s->ext.ech.innerch_len
+                                   - SSL3_HM_HEADER_LENGTH)
+                || !WPACKET_finish(&inner)) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+        } else if (s->ext.ech.es != NULL) {
+            PACKET newpkt;
+
+            if (ossl_ech_early_decrypt(s, pkt, &newpkt) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                goto err;
+            }
+            if (s->ext.ech.success == 1) {
+                /*
+                 * Replace the outer CH with the inner, as long as there's
+                 * space, which there better be! (a bug triggered a bigger
+                 * inner CH once;-)
+                 */
+                if (PACKET_remaining(&newpkt) > PACKET_remaining(pkt)) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    goto err;
+                }
+                *pkt = newpkt;
+            }
+        }
+    }
+#endif
+
     /* Check if this is actually an unexpected renegotiation ClientHello */
     if (s->renegotiate == 0 && !SSL_IS_FIRST_HANDSHAKE(s)) {
         if (!ossl_assert(!SSL_CONNECTION_IS_TLS13(s))) {
@@ -1696,6 +1780,12 @@ MSG_PROCESS_RETURN tls_process_client_hello(SSL_CONNECTION *s, PACKET *pkt)
     if (clienthello != NULL)
         OPENSSL_free(clienthello->pre_proc_exts);
     OPENSSL_free(clienthello);
+#ifndef OPENSSL_NO_ECH
+    s->clienthello = NULL;
+    OPENSSL_free(s->ext.ech.innerch);
+    s->ext.ech.innerch = NULL;
+    s->ext.ech.innerch_len = 0;
+#endif
 
     return MSG_PROCESS_ERROR;
 }
@@ -1989,12 +2079,24 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s)
         goto err;
     }
 
-    if (!s->hit
-            && s->version >= TLS1_VERSION
-            && !SSL_CONNECTION_IS_TLS13(s)
-            && !SSL_CONNECTION_IS_DTLS(s)
-            && s->ext.session_secret_cb != NULL) {
+    /*
+     * Unless ECH has worked or not been configured we won't call
+     * the session_secret_cb now because we'll need to calculate the
+     * server random later to include the ECH accept value.
+     * We can't do it now as we don't yet have the SH encoding.
+     */
+    if (
+#ifndef OPENSSL_NO_ECH
+        ((s->ext.ech.es != NULL && s->ext.ech.success == 1)
+         || s->ext.ech.es == NULL) &&
+#endif
+        !s->hit
+        && s->version >= TLS1_VERSION
+        && !SSL_CONNECTION_IS_TLS13(s)
+        && !SSL_CONNECTION_IS_DTLS(s)
+        && s->ext.session_secret_cb != NULL) {
         const SSL_CIPHER *pref_cipher = NULL;
+
         /*
          * s->session->master_key_length is a size_t, but this is an int for
          * backwards compat reasons
@@ -2151,7 +2253,8 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s)
  err:
     sk_SSL_CIPHER_free(ciphers);
     sk_SSL_CIPHER_free(scsvs);
-    OPENSSL_free(clienthello->pre_proc_exts);
+    if (clienthello != NULL)
+        OPENSSL_free(clienthello->pre_proc_exts);
     OPENSSL_free(s->clienthello);
     s->clienthello = NULL;
 
@@ -2513,16 +2616,147 @@ CON_FUNC_RETURN tls_construct_server_hello(SSL_CONNECTION *s, WPACKET *pkt)
          * Re-initialise the Transcript Hash. We're going to prepopulate it with
          * a synthetic message_hash in place of ClientHello1.
          */
-        if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0)) {
+#ifndef OPENSSL_NO_ECH
+        /*
+         * if we're sending 2nd SH after HRR and we did ECH
+         * then we want to inject the hash of the inner CH1
+         * and not the outer (which is the default)
+         */
+        OSSL_TRACE_BEGIN(TLS) {
+            BIO_printf(trc_out, "Checking success (%d)/innerCH (%p)\n",
+                       s->ext.ech.success, (void *)s->ext.ech.innerch);
+        } OSSL_TRACE_END(TLS);
+        if ((s->ext.ech.backend == 1 || s->ext.ech.success == 1)
+            && s->ext.ech.innerch != NULL) {
+            /* do pre-existing HRR stuff */
+            unsigned char hashval[EVP_MAX_MD_SIZE];
+            unsigned int hashlen;
+            EVP_MD_CTX *ctx = EVP_MD_CTX_new();
+            const EVP_MD *md = NULL;
+
+            OSSL_TRACE(TLS, "Adding in digest of ClientHello\n");
+# ifdef OSSL_ECH_SUPERVERBOSE
+            ossl_ech_pbuf("innerch", s->ext.ech.innerch,
+                          s->ext.ech.innerch_len);
+# endif
+            if (ctx == NULL) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return CON_FUNC_ERROR;
+            }
+            md = ssl_handshake_md(s);
+            if (md == NULL) {
+                EVP_MD_CTX_free(ctx);
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return CON_FUNC_ERROR;
+            }
+            if (EVP_DigestInit_ex(ctx, md, NULL) <= 0
+                || EVP_DigestUpdate(ctx, s->ext.ech.innerch,
+                                    s->ext.ech.innerch_len) <= 0
+                || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) {
+                EVP_MD_CTX_free(ctx);
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return CON_FUNC_ERROR;
+            }
+# ifdef OSSL_ECH_SUPERVERBOSE
+            ossl_ech_pbuf("digested CH", hashval, hashlen);
+# endif
+            EVP_MD_CTX_free(ctx);
+            if (ossl_ech_reset_hs_buffer(s, NULL, 0) != 1) {
+                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                return CON_FUNC_ERROR;
+            }
+            if (!create_synthetic_message_hash(s, hashval, hashlen, NULL, 0)) {
+                /* SSLfatal() already called */
+                return CON_FUNC_ERROR;
+            }
+        } else {
+            if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0))
+                return CON_FUNC_ERROR; /* SSLfatal() already called */
+        }
+#else
+        if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0))
             /* SSLfatal() already called */
             return CON_FUNC_ERROR;
-        }
+#endif /* OPENSSL_NO_ECH */
     } else if (!(s->verify_mode & SSL_VERIFY_PEER)
-                && !ssl3_digest_cached_records(s, 0)) {
+               && !ssl3_digest_cached_records(s, 0)) {
         /* SSLfatal() already called */;
         return CON_FUNC_ERROR;
     }
 
+#ifndef OPENSSL_NO_ECH
+    /*
+     * Calculate the ECH-accept server random to indicate that
+     * we're accepting ECH, if that's the case
+     */
+    if (s->ext.ech.attempted_type == TLSEXT_TYPE_ech
+        && (s->ext.ech.backend == 1
+            || (s->ext.ech.es != NULL && s->ext.ech.success == 1))) {
+        unsigned char acbuf[8];
+        unsigned char *shbuf = NULL;
+        size_t shlen = 0;
+        size_t shoffset = 0;
+        int hrr = 0;
+
+        if (s->hello_retry_request == SSL_HRR_PENDING)
+            hrr = 1;
+        memset(acbuf, 0, 8);
+        if (WPACKET_get_total_written(pkt, &shlen) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return CON_FUNC_ERROR;
+        }
+        shbuf = WPACKET_get_curr(pkt) - shlen;
+        /* we need to fixup SH length here */
+        shbuf[1] = ((shlen - 4)) >> 16 & 0xff;
+        shbuf[2] = ((shlen - 4)) >> 8 & 0xff;
+        shbuf[3] = (shlen - 4) & 0xff;
+        if (ossl_ech_intbuf_add(s, shbuf, shlen, hrr) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return CON_FUNC_ERROR;
+        }
+        if (ossl_ech_calc_confirm(s, hrr, acbuf, shlen) != 1) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return CON_FUNC_ERROR;
+        }
+        memcpy(s->s3.server_random + SSL3_RANDOM_SIZE - 8, acbuf, 8);
+        if (hrr == 0) {
+            /* confirm value hacked into SH.random rightmost octets */
+            shoffset = SSL3_HM_HEADER_LENGTH /* 4 */
+                + CLIENT_VERSION_LEN /* 2 */
+                + SSL3_RANDOM_SIZE /* 32 */
+                - 8;
+            memcpy(shbuf + shoffset, acbuf, 8);
+        } else {
+            /*
+             * confirm value is in extension in HRR case as the SH.random
+             * is already hacked to be a specific value in a HRR
+             */
+            memcpy(WPACKET_get_curr(pkt) - 8, acbuf, 8);
+        }
+    }
+    /* call ECH callback, if appropriate */
+    if (s->ext.ech.attempted == 1 && s->ext.ech.cb != NULL
+        && s->hello_retry_request != SSL_HRR_PENDING) {
+        char pstr[OSSL_ECH_PBUF_SIZE + 1];
+        BIO *biom = BIO_new(BIO_s_mem());
+        unsigned int cbrv = 0;
+
+        if (biom == NULL) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return CON_FUNC_ERROR;
+        }
+        memset(pstr, 0, OSSL_ECH_PBUF_SIZE + 1);
+        ossl_ech_status_print(biom, s, OSSL_ECHSTORE_ALL);
+        BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE);
+        cbrv = s->ext.ech.cb(&s->ssl, pstr);
+        BIO_free(biom);
+        if (cbrv != 1) {
+            OSSL_TRACE(TLS, "Error from tls_construct_server_hello/ech_cb\n");
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return CON_FUNC_ERROR;
+        }
+    }
+#endif /* OPENSSL_NO_ECH */
     return CON_FUNC_SUCCESS;
 }
 
diff --git a/ssl/t1_enc.c b/ssl/t1_enc.c
index 941455bc8aeb2..bf7a7a516f7e6 100644
--- a/ssl/t1_enc.c
+++ b/ssl/t1_enc.c
@@ -585,6 +585,10 @@ int tls1_alert_code(int code)
         return SSL_AD_HANDSHAKE_FAILURE;
     case TLS13_AD_MISSING_EXTENSION:
         return SSL_AD_HANDSHAKE_FAILURE;
+#ifndef OPENSSL_NO_ECH
+    case SSL_AD_ECH_REQUIRED:
+        return TLS1_AD_ECH_REQUIRED;
+#endif
     default:
         return -1;
     }
diff --git a/ssl/t1_trce.c b/ssl/t1_trce.c
index 640552c0ba0db..e6dcf28db301d 100644
--- a/ssl/t1_trce.c
+++ b/ssl/t1_trce.c
@@ -500,6 +500,10 @@ static const ssl_trace_tbl ssl_exts_tbl[] = {
 # ifndef OPENSSL_NO_NEXTPROTONEG
     {TLSEXT_TYPE_next_proto_neg, "next_proto_neg"},
 # endif
+# ifndef OPENSSL_NO_ECH
+    {TLSEXT_TYPE_ech, "encrypted_client_hello"},
+    {TLSEXT_TYPE_outer_extensions, "outer_extension"},
+# endif
 };
 
 static const ssl_trace_tbl ssl_groups_tbl[] = {
diff --git a/ssl/tls13_enc.c b/ssl/tls13_enc.c
index 489fafd521447..254aea063ea39 100644
--- a/ssl/tls13_enc.c
+++ b/ssl/tls13_enc.c
@@ -529,10 +529,25 @@ int tls13_change_cipher_state(SSL_CONNECTION *s, int which)
             labellen = sizeof(client_early_traffic) - 1;
             log_label = CLIENT_EARLY_LABEL;
 
-            handlen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
-            if (handlen <= 0) {
-                SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH);
-                goto err;
+#ifndef OPENSSL_NO_ECH
+            /* if ECH worked then use the innerch and not the h/s buffer here */
+            if (((which & SSL3_CC_SERVER) && s->ext.ech.success == 1)
+                || ((which & SSL3_CC_CLIENT) && s->ext.ech.attempted == 1)) {
+                if (s->ext.ech.innerch == NULL) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    goto err;
+                }
+                handlen = (long)s->ext.ech.innerch_len;
+                hdata = s->ext.ech.innerch;
+            } else
+#endif
+            {
+                handlen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
+                if (handlen <= 0) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR,
+                            SSL_R_BAD_HANDSHAKE_LENGTH);
+                    goto err;
+                }
             }
 
             if (s->early_data_state == SSL_EARLY_DATA_CONNECTING
diff --git a/test/build.info b/test/build.info
index 57ee94071a822..34c9fd4ab59ee 100644
--- a/test/build.info
+++ b/test/build.info
@@ -70,7 +70,8 @@ IF[{- !$disabled{tests} -}]
           ca_internals_test bio_tfo_test membio_test bio_dgram_test list_test \
           fips_version_test x509_test hpke_test pairwise_fail_test \
           nodefltctxtest evp_xof_test x509_load_cert_file_test bio_meth_test \
-          x509_acert_test x509_req_test strtoultest bio_pw_callback_test
+          x509_acert_test x509_req_test strtoultest bio_pw_callback_test \
+          ech_test
 
   IF[{- !$disabled{'rpk'} -}]
     PROGRAMS{noinst}=rpktest
@@ -233,6 +234,10 @@ IF[{- !$disabled{tests} -}]
     DEPEND[lms_test]=../libcrypto.a libtestutil.a
   ENDIF
 
+  SOURCE[ech_test]=ech_test.c helpers/ssltestlib.c
+  INCLUDE[ech_test]=../include ../apps/include
+  DEPEND[ech_test]=../libssl.a ../libcrypto.a libtestutil.a
+
   SOURCE[evp_extra_test2]=evp_extra_test2.c $INITSRC tls-provider.c
   INCLUDE[evp_extra_test2]=../include ../apps/include
   DEPEND[evp_extra_test2]=../libcrypto libtestutil.a
diff --git a/test/certs/ech-big.pem b/test/certs/ech-big.pem
new file mode 100644
index 0000000000000..99c9c67bdee4d
--- /dev/null
+++ b/test/certs/ech-big.pem
@@ -0,0 +1,25 @@
+-----BEGIN ECHCONFIG-----
+BNj+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs
+ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl
+eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB
+AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW
+PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q
+LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu
+WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc
+8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv
+EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF
+/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBi
+x2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7
+ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA
+/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUu
+Y29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhh
+bXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQAB
+AAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwA
+BAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxp
+c2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpC
+lg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd
+8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMs
+pDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4R
+CERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA
+-----END ECHCONFIG-----
+
diff --git a/test/certs/ech-eg.pem b/test/certs/ech-eg.pem
new file mode 100644
index 0000000000000..4d37f5b17d54a
--- /dev/null
+++ b/test/certs/ech-eg.pem
@@ -0,0 +1,7 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VuBCIEIKBC3rocwIF5tGY+/TaYQrCxY+ULsch94ja9DojkcvlT
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+ADn+DQA1agAgACBtuySC1pphjFlGYKTaSm2KWNg7GQVRS8uAYvLTm5QlGwAEAAEA
+AQAGZWcuY29tAAA=
+-----END ECHCONFIG-----
diff --git a/test/certs/ech-giant.pem b/test/certs/ech-giant.pem
new file mode 100644
index 0000000000000..d0e5a46c4134d
--- /dev/null
+++ b/test/certs/ech-giant.pem
@@ -0,0 +1,37 @@
+-----BEGIN ECHCONFIG-----
+B8D+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs
+ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl
+eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB
+AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW
+PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q
+LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu
+WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc
+8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv
+EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF
+/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBi
+x2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7
+ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA
+/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUu
+Y29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhh
+bXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQAB
+AAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwA
+BAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxp
+c2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpC
+lg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd
+8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMs
+pDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4R
+CERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdg
+e/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAg
+ACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4N
+ADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNv
+bQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1w
+bGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQAL
+ZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQA
+AQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNg
+FjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP
+0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFV
+LlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQz
+nPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhE
+bxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvy
+xf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA
+-----END ECHCONFIG-----`
diff --git a/test/certs/ech-mid.pem b/test/certs/ech-mid.pem
new file mode 100644
index 0000000000000..7c5aa86e144eb
--- /dev/null
+++ b/test/certs/ech-mid.pem
@@ -0,0 +1,11 @@
+-----BEGIN ECHCONFIG-----
+AfD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs
+ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl
+eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB
+AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW
+PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q
+LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu
+WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc
+8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv
+EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA
+-----END ECHCONFIG-----
diff --git a/test/certs/ech-rsa.pem b/test/certs/ech-rsa.pem
new file mode 100644
index 0000000000000..17b23cf04f5e0
--- /dev/null
+++ b/test/certs/ech-rsa.pem
@@ -0,0 +1,14 @@
+-----BEGIN PRIVATE KEY-----
+MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEApeb9fP5SDxyOQZQT
+qGg2QeE0ypxY6Th33aDkRCRVB69rDMSA1Thfeyk65IfaPaA3bC4hsqAIBgslcFfk
+1/i8KQIDAQABAkAsH3EPizwb1MZo3o8T3ROBFfpKYKas8F3Azgenr9oFfs5kPgya
+VDdtZu+UweG5nTo+fZG5ZFmcwWXJTLtiUfABAiEAz2gvTuc0lPTQi3t6RFB5nGCt
+h75Ofx/ceusHa2a36QECIQDMxXJQnuWY+bH/wSfPY/ySltQ6U2cy0LHQ37FIfSFr
+KQIgUo++hUI0BDeP7HYyrY77WeyCJ07yIFimg6ebRH2XKAECIQCSavhTd1q6qIhD
+VMzveRInixvTXMGkzx7mOJzeNUMJCQIhAJjjVdRjUpWPMquRDCddmwegh88ptsFX
+T/Ygm1OubAyM
+-----END PRIVATE KEY-----
+-----BEGIN ECHCONFIG-----
+AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA
+AQALZXhhbXBsZS5jb20AAA==
+-----END ECHCONFIG-----
diff --git a/test/certs/echserver.key b/test/certs/echserver.key
new file mode 100644
index 0000000000000..372878ce57385
--- /dev/null
+++ b/test/certs/echserver.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCJHNs4e27KjdYU
+8IgiT539WbEl16Eve6tu1UFpGdkqsHH8+yceoFkMWSdpr+Sh3PYDRk/Ek0qB33uK
+y3FKlIejtolxVhBybtja5zYMmVXnRHsB/qe7FgyM/lv0xtO1nfSGFkVZVz1+xWPr
+aslJN3U4HPaaL4SGghw5KIRD8FPx174v8FDOBeVhn6pzTK/xpTeqXLvAAxgPhF+Q
+HOQ0pTXrOHbaiK4l+8JfVm+0fCJjMnT20mmGuTNjvdWZ4XIgPPYkEQrf1CpdONcU
+kiiFcpcYtbVS0YyC91qqJLKFv51eki9STwUQISn5jLWIRQlXkBXhK6aGlkLWnov/
+kqqUWEQTAgMBAAECggEAAah0LDAt7EwfyRwJgWS2E+C4SC1d2R2lOo9gnZ0+54m/
+rx/4XqHwwbn4RIpoeN6bqPl6MHXZgk2KCGkiYxT9uOiVq+WvCDs36xm9qRRXmhbV
+Z/ZE3/nJyBCxWvnmiH0y/kYZq5Vm/Hf1l9ywN27wv292OfIWJ6w+HCDVzJ6E3VlK
+fuzBFhZmnBjul6Nlo76blNXwn5loWYomkg6nVWzrTjYWosGd0aKpZJR948nWpuEj
+fkevLqMMfSuB+cXQ+zB4lttqB5dphFxbNv5gHOd1rllzHFdhK+/7e7ktGYmpQtuH
+WRKPD1y173ek5FtvTxtcrL2rST+hoSDWcCQCws/70QKBgQC5QGlldraTs0JXhoH9
+6X+V1mvsAWCItq7JhUvFHFtAxHuYacrlnsJRxv8aRS8AhuNYTtThJQcWzFL2UU7W
+CdiB0VZr7phPNOVYsa95V8a8A1CHllfdTzxw1TyiOJ0sdeU7irWo3vnTDxftfySP
+lkdPNbItO1RXqeIR6mJf+rVGowKBgQC9egaDDdM2YsMtl61fIRoPMPdJB3fGcL0A
+FwUAtGQ1twETykzcUCeAqx0yx7zCAyPeA9WmpHzuz/LR8uA3TP/nDcLlCaQowfeR
+VPdS3Q1iAnSyaCsF1THQPvhFsYMXbIn/svSSpddLOrP6ltSr+PORLOXXUmQJnffk
+hxKaxK6T0QKBgFzyVm9UGtMMk/K6SCqPpzYUuV1Wa4rsrdHqkVO6oIZkjuav3d9L
+wo+pWoFhyO1owFSkaOb13xKvPcjcjsOReRHZaJUKx1ymW5Qewr4NLmdS+mqtIjSl
+9tteAegao7GVDYjMVcz+4zXkUssUidGJQwoZFObg57Z8RDNc+DLT5XQlAoGAYaO8
+L1S0ftYuFhSPdvIr56AoDi4W/t+hxaYXIeHTsgp4N6aMLQvxD1EeXsim8KOFnCcF
+tjYVW0s1qhMqj9TSGlLxF+379jTeSroqKT1YZCU31afwY7UVUmbgsalkEHISOv4R
+InDrnQzHKl8HgQdtHGayml8OxhXtZIpmf/LSs8ECgYBrFbKl8ylhlzw5rC8DuP8n
+hzKLOKzipKmHLn4eDBEFyLTyoyYrqx/nxLi3kSIyNP4fJ9vHOXgdjdrp9xRMcFEx
+IA2sdywI5VuymxktP8OlORa0NK4eFZXkDNsQlkathYiKqCwGjUWdGk5+Ry5qO/UC
+9ua9adjNa108aBzWLYZFCw==
+-----END PRIVATE KEY-----
diff --git a/test/certs/echserver.pem b/test/certs/echserver.pem
new file mode 100644
index 0000000000000..11e0617e887fe
--- /dev/null
+++ b/test/certs/echserver.pem
@@ -0,0 +1,80 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number:
+            18:45:8f:30:1d:fe:dc:22:9d:95:40:8c:e5:36:f9:38:0d:d5:58:a0
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=Root CA
+        Validity
+            Not Before: Oct  6 18:36:12 2023 GMT
+            Not After : Sep 12 18:36:12 2123 GMT
+        Subject: CN=server.example
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:89:1c:db:38:7b:6e:ca:8d:d6:14:f0:88:22:4f:
+                    9d:fd:59:b1:25:d7:a1:2f:7b:ab:6e:d5:41:69:19:
+                    d9:2a:b0:71:fc:fb:27:1e:a0:59:0c:59:27:69:af:
+                    e4:a1:dc:f6:03:46:4f:c4:93:4a:81:df:7b:8a:cb:
+                    71:4a:94:87:a3:b6:89:71:56:10:72:6e:d8:da:e7:
+                    36:0c:99:55:e7:44:7b:01:fe:a7:bb:16:0c:8c:fe:
+                    5b:f4:c6:d3:b5:9d:f4:86:16:45:59:57:3d:7e:c5:
+                    63:eb:6a:c9:49:37:75:38:1c:f6:9a:2f:84:86:82:
+                    1c:39:28:84:43:f0:53:f1:d7:be:2f:f0:50:ce:05:
+                    e5:61:9f:aa:73:4c:af:f1:a5:37:aa:5c:bb:c0:03:
+                    18:0f:84:5f:90:1c:e4:34:a5:35:eb:38:76:da:88:
+                    ae:25:fb:c2:5f:56:6f:b4:7c:22:63:32:74:f6:d2:
+                    69:86:b9:33:63:bd:d5:99:e1:72:20:3c:f6:24:11:
+                    0a:df:d4:2a:5d:38:d7:14:92:28:85:72:97:18:b5:
+                    b5:52:d1:8c:82:f7:5a:aa:24:b2:85:bf:9d:5e:92:
+                    2f:52:4f:05:10:21:29:f9:8c:b5:88:45:09:57:90:
+                    15:e1:2b:a6:86:96:42:d6:9e:8b:ff:92:aa:94:58:
+                    44:13
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: 
+                CA:FALSE
+            X509v3 Subject Key Identifier: 
+                8C:E0:38:04:70:7E:B4:CB:1F:BF:AA:E6:67:42:74:63:46:88:58:74
+            X509v3 Authority Key Identifier: 
+                70:7F:2E:AE:83:68:59:98:04:23:2A:CD:EB:3E:17:CD:24:DD:01:49
+            X509v3 Subject Alternative Name: 
+                DNS:*.server.example, DNS:server.example
+    Signature Algorithm: sha256WithRSAEncryption
+    Signature Value:
+        9b:fe:bc:b1:40:d4:08:91:f6:1f:b4:0f:8c:50:ac:49:36:6f:
+        27:93:e8:94:13:bc:fe:1a:2a:cf:93:98:13:b3:b4:85:a5:62:
+        4d:58:8f:da:cd:f7:1b:c3:1f:42:ba:2a:89:45:11:33:49:86:
+        2c:3a:0a:99:17:4f:0c:f1:1e:35:31:2c:69:f9:15:d5:37:54:
+        cc:9e:e3:67:9f:d5:6e:ad:b1:26:60:df:aa:84:63:da:a7:31:
+        c9:69:a0:d8:c2:96:d3:82:b4:99:70:8c:3c:92:a4:c0:f0:7c:
+        3f:04:d3:29:4f:6c:c5:fd:39:12:95:65:7f:37:fb:52:5b:12:
+        99:d6:d7:b5:ba:44:6e:36:ec:5d:f2:5d:d4:aa:2d:8a:46:ce:
+        29:66:c1:ed:36:13:f2:f3:ae:92:4a:97:db:99:ed:8f:4e:4e:
+        ed:73:1b:fa:3e:64:63:40:5c:c2:03:76:2c:dc:58:01:3f:17:
+        d0:ae:a6:b2:64:85:47:ba:7d:5a:36:53:e4:90:00:8e:f5:17:
+        a5:ff:a3:81:ee:ed:25:ca:10:76:75:2d:65:ff:f8:b1:8c:3c:
+        a3:ff:81:12:72:c7:bc:b5:17:06:d8:c6:13:97:cb:8e:58:51:
+        2a:a4:be:91:59:40:4b:07:8d:69:2f:92:ee:ea:9c:bf:eb:42:
+        b7:62:b8:e3
+-----BEGIN CERTIFICATE-----
+MIIDNTCCAh2gAwIBAgIUGEWPMB3+3CKdlUCM5Tb5OA3VWKAwDQYJKoZIhvcNAQEL
+BQAwEjEQMA4GA1UEAwwHUm9vdCBDQTAgFw0yMzEwMDYxODM2MTJaGA8yMTIzMDkx
+MjE4MzYxMlowGTEXMBUGA1UEAwwOc2VydmVyLmV4YW1wbGUwggEiMA0GCSqGSIb3
+DQEBAQUAA4IBDwAwggEKAoIBAQCJHNs4e27KjdYU8IgiT539WbEl16Eve6tu1UFp
+GdkqsHH8+yceoFkMWSdpr+Sh3PYDRk/Ek0qB33uKy3FKlIejtolxVhBybtja5zYM
+mVXnRHsB/qe7FgyM/lv0xtO1nfSGFkVZVz1+xWPraslJN3U4HPaaL4SGghw5KIRD
+8FPx174v8FDOBeVhn6pzTK/xpTeqXLvAAxgPhF+QHOQ0pTXrOHbaiK4l+8JfVm+0
+fCJjMnT20mmGuTNjvdWZ4XIgPPYkEQrf1CpdONcUkiiFcpcYtbVS0YyC91qqJLKF
+v51eki9STwUQISn5jLWIRQlXkBXhK6aGlkLWnov/kqqUWEQTAgMBAAGjejB4MAkG
+A1UdEwQCMAAwHQYDVR0OBBYEFIzgOARwfrTLH7+q5mdCdGNGiFh0MB8GA1UdIwQY
+MBaAFHB/Lq6DaFmYBCMqzes+F80k3QFJMCsGA1UdEQQkMCKCECouc2VydmVyLmV4
+YW1wbGWCDnNlcnZlci5leGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQCb/ryxQNQI
+kfYftA+MUKxJNm8nk+iUE7z+GirPk5gTs7SFpWJNWI/azfcbwx9CuiqJRREzSYYs
+OgqZF08M8R41MSxp+RXVN1TMnuNnn9VurbEmYN+qhGPapzHJaaDYwpbTgrSZcIw8
+kqTA8Hw/BNMpT2zF/TkSlWV/N/tSWxKZ1te1ukRuNuxd8l3Uqi2KRs4pZsHtNhPy
+866SSpfbme2PTk7tcxv6PmRjQFzCA3Ys3FgBPxfQrqayZIVHun1aNlPkkACO9Rel
+/6OB7u0lyhB2dS1l//ixjDyj/4EScse8tRcG2MYTl8uOWFEqpL6RWUBLB41pL5Lu
+6py/60K3Yrjj
+-----END CERTIFICATE-----
diff --git a/test/ech_test.c b/test/ech_test.c
new file mode 100644
index 0000000000000..32182125a98d0
--- /dev/null
+++ b/test/ech_test.c
@@ -0,0 +1,1524 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include 
+#include 
+#include "testutil.h"
+#include "helpers/ssltestlib.h"
+#include "internal/packet.h"
+
+#ifndef OPENSSL_NO_ECH
+
+# define DEF_CERTS_DIR "test/certs"
+
+static OSSL_LIB_CTX *libctx = NULL;
+static char *propq = NULL;
+static int verbose = 0;
+static char *certsdir = NULL;
+static char *cert = NULL;
+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");
+    return 1;
+}
+
+/* ClientHello callback */
+static int ch_test_cb(SSL *ssl, int *al, void *arg)
+{
+    char *servername = NULL;
+    const unsigned char *pos;
+    size_t remaining;
+    unsigned int servname_type;
+    PACKET pkt, sni, hostname;
+
+    if (verbose) {
+        TEST_info("ch_test_cb called");
+        if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_ech, &pos, &remaining)) {
+            TEST_info("there is an ECH extension");
+        } else {
+            TEST_info("there is NO ECH extension");
+        }
+    }
+    if (!SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &pos,
+                                   &remaining)
+            || remaining <= 2)
+        goto give_up;
+    if (!PACKET_buf_init(&pkt, pos, remaining)
+        || !PACKET_as_length_prefixed_2(&pkt, &sni)
+        || !PACKET_get_1(&sni, &servname_type)
+        || servname_type != TLSEXT_NAMETYPE_host_name
+        || !PACKET_as_length_prefixed_2(&sni, &hostname)
+        || (PACKET_remaining(&hostname) > TLSEXT_MAXLEN_host_name)
+        || PACKET_contains_zero_byte(&hostname)
+        || !PACKET_strndup(&hostname, &servername))
+        goto give_up;
+    if (verbose)
+        TEST_info("servername: %s", servername);
+    OPENSSL_free(servername);
+    /* signal to caller all is good */
+    ch_test_cb_ok = 1;
+    return 1;
+give_up:
+    return 0;
+}
+
+/*
+ * The define/vars below and the 3 callback functions are modified
+ * from test/sslapitest.c
+ */
+# define TEST_EXT_TYPE1  0xffab /* custom ext type 1: has 1 octet payload */
+# define TEST_EXT_TYPE2  0xffcd /* custom ext type 2: no payload */
+
+/* A well-encoded ECH extension value */
+static const unsigned char encoded_ech_val[] = {
+    0x00, 0x00, 0x01, 0x00, 0x01, 0xf7, 0x00, 0x20,
+    0xc9, 0x2c, 0x12, 0xc9, 0xc0, 0x4d, 0x11, 0x5d,
+    0x09, 0xe1, 0xeb, 0x7a, 0x18, 0xb2, 0x83, 0x28,
+    0x35, 0x00, 0x3c, 0x8d, 0x78, 0x09, 0xfd, 0x09,
+    0x84, 0xca, 0x94, 0x77, 0xcf, 0x78, 0xd0, 0x04,
+    0x00, 0x90, 0x5e, 0xc7, 0xc0, 0x62, 0x84, 0x8d,
+    0x4b, 0x85, 0xd5, 0x6a, 0x9a, 0xc1, 0xc6, 0xc2,
+    0x28, 0xac, 0x87, 0xb9, 0x2f, 0x36, 0xa0, 0xf7,
+    0x5f, 0xd0, 0x23, 0x7b, 0xf4, 0xc1, 0x62, 0x1c,
+    0xf1, 0x91, 0xfd, 0x46, 0x35, 0x41, 0xc9, 0x06,
+    0xd3, 0x19, 0xd6, 0x34, 0x01, 0xc3, 0xb3, 0x66,
+    0x4e, 0x7a, 0x28, 0xac, 0xd4, 0xd2, 0x35, 0x2b,
+    0xd0, 0xc6, 0x94, 0x34, 0xc1, 0x94, 0x62, 0x77,
+    0x1b, 0x5a, 0x02, 0x3c, 0xdd, 0xa2, 0x4d, 0x33,
+    0xa5, 0xd0, 0x59, 0x12, 0xf5, 0x17, 0x03, 0xe5,
+    0xab, 0xbd, 0x83, 0x52, 0x40, 0x6c, 0x99, 0xac,
+    0x25, 0x07, 0x63, 0x8c, 0x16, 0x5d, 0x93, 0x34,
+    0x56, 0x34, 0x60, 0x86, 0x25, 0xa7, 0x0d, 0xac,
+    0xb8, 0x5e, 0x87, 0xc6, 0xf7, 0x23, 0xaf, 0xf8,
+    0x3e, 0x2a, 0x46, 0x75, 0xa9, 0x5f, 0xaf, 0xd2,
+    0x91, 0xe6, 0x44, 0xcb, 0xe7, 0xe0, 0x85, 0x36,
+    0x9d, 0xd2, 0xaf, 0xae, 0xb3, 0x0f, 0x70, 0x6a,
+    0xaf, 0x42, 0xc0, 0xb3, 0xe4, 0x65, 0x53, 0x01,
+    0x75, 0xbf
+};
+
+static int new_add_cb(SSL *s, unsigned int ext_type, unsigned int context,
+                      const unsigned char **out, size_t *outlen, X509 *x,
+                      size_t chainidx, int *al, void *add_arg)
+{
+    int *server = (int *)add_arg;
+    unsigned char *data;
+
+    if (*server != SSL_is_server(s))
+        return -1;
+    if (ext_type == TEST_EXT_TYPE1) {
+        if ((data = OPENSSL_malloc(sizeof(*data))) == NULL)
+            return -1;
+        *data = 1;
+        *out = data;
+        *outlen = sizeof(*data);
+    } else if (ext_type == OSSL_ECH_CURRENT_VERSION) {
+        /* inject a sample ECH extension value into the CH */
+        if ((data = OPENSSL_memdup(encoded_ech_val,
+                                   sizeof(encoded_ech_val))) == NULL)
+            return -1;
+        *out = data;
+        *outlen = sizeof(encoded_ech_val);
+    } else {
+        /* inject a TEST_EXT_TYPE2, with a zero-length payload */
+        *out = NULL;
+        *outlen = 0;
+    }
+    return 1;
+}
+
+static void new_free_cb(SSL *s, unsigned int ext_type, unsigned int context,
+                        const unsigned char *out, void *add_arg)
+{
+    OPENSSL_free((unsigned char *)out);
+}
+
+static int new_parse_cb(SSL *s, unsigned int ext_type, unsigned int context,
+                        const unsigned char *in, size_t inlen, X509 *x,
+                        size_t chainidx, int *al, void *parse_arg)
+{
+    int *server = (int *)parse_arg;
+
+    if (*server != SSL_is_server(s)
+            || inlen != sizeof(char) || *in != 1)
+        return -1;
+    return 1;
+}
+
+/* general test vector values */
+
+/* standard x25519 ech key pair with public key example.com */
+static const char pem_kp1[] =
+    "-----BEGIN PRIVATE KEY-----\n"
+    "MC4CAQAwBQYDK2VuBCIEILDIeo9Eqc4K9/uQ0PNAyMaP60qrxiSHT2tNZL3ksIZS\n"
+    "-----END PRIVATE KEY-----\n"
+    "-----BEGIN ECHCONFIG-----\n"
+    "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
+    "AQALZXhhbXBsZS5jb20AAA==\n"
+    "-----END ECHCONFIG-----\n";
+
+/* standard x25519 ECHConfigList with public key example.com */
+static const char pem_pk1[] =
+    "-----BEGIN ECHCONFIG-----\n"
+    "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
+    "AQALZXhhbXBsZS5jb20AAA==\n"
+    "-----END ECHCONFIG-----\n";
+
+/* an ECDSA private with an x25519 ech public key example.com */
+static const char pem_mismatch_priv[] =
+    "-----BEGIN EC PRIVATE KEY-----\n"
+    "MHcCAQEEIGKONznbHOMEKT4AKMufc37O9lUEBHO+Nb6ztkXhGXLcoAoGCCqGSM49\n"
+    "AwEHoUQDQgAEYDznfezvj5ufhQsZOQvSdiNpYKCd8tRI1aI3gc4y7gmdDUKpwzHa\n"
+    "VS4Qq0xyeG6fDMJv668UCotQANFsifGirQ==\n"
+    "-----END EC PRIVATE KEY-----\n"
+    "-----BEGIN ECHCONFIG-----\n"
+    "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
+    "AQALZXhhbXBsZS5jb20AAA==\n"
+    "-----END ECHCONFIG-----\n";
+
+/*
+ * This ECHConfigList has 4 entries with different versions,
+ * from drafts: 13,10,13,9 - since our runtime no longer supports
+ * version 9 or 10, we should see 2 configs loaded.
+ */
+static const char pem_4_to_2[] =
+    "-----BEGIN ECHCONFIG-----\n"
+    "APv+DQA6xQAgACBm54KSIPXu+pQq2oY183wt3ybx7CKbBYX0ogPq5u6FegAEAAEA\n"
+    "AQALZXhhbXBsZS5jb20AAP4KADzSACAAIIP+0Qt0WGBF3H5fz8HuhVRTCEMuHS4K\n"
+    "hu6ibR/6qER4AAQAAQABAAAAC2V4YW1wbGUuY29tAAD+DQA6QwAgACB3xsNUtSgi\n"
+    "piYpUkW6OSrrg03I4zIENMFa0JR2+Mm1WwAEAAEAAQALZXhhbXBsZS5jb20AAP4J\n"
+    "ADsAC2V4YW1wbGUuY29tACCjJCv5w/yaHjbOc6nVuM/GksIGLgDR+222vww9dEk8\n"
+    "FwAgAAQAAQABAAAAAA==\n"
+    "-----END ECHCONFIG-----\n";
+
+/* mis-spelled PEM string */
+static const char pem_typo[] =
+    "-----BEGIN PRIVATE KEY-----\n"
+    "MC4CAQAwBQYDK2VuBCIEILDIeo9Eqc4K9/uQ0PNAyMaP60qrxiSHT2tNZL3ksIZS\n"
+    "-----END PRIVATE KEY-----\n"
+    "-----BEGIN ExHCOxFIG-----\n"
+    "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n"
+    "AQALZXhhbXBsZS5jb20AAA==\n"
+    "-----END ExHCOxFIG-----\n";
+
+/* single-line base64(ECHConfigList) form of pem_pk1 */
+static const char b64_pk1[] =
+    "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA"
+    "AQALZXhhbXBsZS5jb20AAA==";
+
+/* single-line base64(ECHConfigList) form of pem_6_to3 */
+static const char b64_6_to_3[] =
+    "AXn+DQA6xQAgACBm54KSIPXu+pQq2oY183wt3ybx7CKbBYX0ogPq5u6FegAEAAE"
+    "AAQALZXhhbXBsZS5jb20AAP4KADzSACAAIIP+0Qt0WGBF3H5fz8HuhVRTCEMuHS"
+    "4Khu6ibR/6qER4AAQAAQABAAAAC2V4YW1wbGUuY29tAAD+CQA7AAtleGFtcGxlL"
+    "mNvbQAgoyQr+cP8mh42znOp1bjPxpLCBi4A0ftttr8MPXRJPBcAIAAEAAEAAQAA"
+    "AAD+DQA6QwAgACB3xsNUtSgipiYpUkW6OSrrg03I4zIENMFa0JR2+Mm1WwAEAAE"
+    "AAQALZXhhbXBsZS5jb20AAP4KADwDACAAIH0BoAdiJCX88gv8nYpGVX5BpGBa9y"
+    "T0Pac3Kwx6i8URAAQAAQABAAAAC2V4YW1wbGUuY29tAAD+DQA6QwAgACDcZIAx7"
+    "OcOiQuk90VV7/DO4lFQr5I3Zw9tVbK8MGw1dgAEAAEAAQALZXhhbXBsZS5jb20A"
+    "AA==";
+
+/* same as above but binary encoded */
+static const unsigned char bin_6_to_3[] = {
+    0x01, 0x79, 0xfe, 0x0d, 0x00, 0x3a, 0xc5, 0x00,
+    0x20, 0x00, 0x20, 0x66, 0xe7, 0x82, 0x92, 0x20,
+    0xf5, 0xee, 0xfa, 0x94, 0x2a, 0xda, 0x86, 0x35,
+    0xf3, 0x7c, 0x2d, 0xdf, 0x26, 0xf1, 0xec, 0x22,
+    0x9b, 0x05, 0x85, 0xf4, 0xa2, 0x03, 0xea, 0xe6,
+    0xee, 0x85, 0x7a, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00,
+    0xfe, 0x0a, 0x00, 0x3c, 0xd2, 0x00, 0x20, 0x00,
+    0x20, 0x83, 0xfe, 0xd1, 0x0b, 0x74, 0x58, 0x60,
+    0x45, 0xdc, 0x7e, 0x5f, 0xcf, 0xc1, 0xee, 0x85,
+    0x54, 0x53, 0x08, 0x43, 0x2e, 0x1d, 0x2e, 0x0a,
+    0x86, 0xee, 0xa2, 0x6d, 0x1f, 0xfa, 0xa8, 0x44,
+    0x78, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00,
+    0x00, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00,
+    0xfe, 0x09, 0x00, 0x3b, 0x00, 0x0b, 0x65, 0x78,
+    0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f,
+    0x6d, 0x00, 0x20, 0xa3, 0x24, 0x2b, 0xf9, 0xc3,
+    0xfc, 0x9a, 0x1e, 0x36, 0xce, 0x73, 0xa9, 0xd5,
+    0xb8, 0xcf, 0xc6, 0x92, 0xc2, 0x06, 0x2e, 0x00,
+    0xd1, 0xfb, 0x6d, 0xb6, 0xbf, 0x0c, 0x3d, 0x74,
+    0x49, 0x3c, 0x17, 0x00, 0x20, 0x00, 0x04, 0x00,
+    0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xfe,
+    0x0d, 0x00, 0x3a, 0x43, 0x00, 0x20, 0x00, 0x20,
+    0x77, 0xc6, 0xc3, 0x54, 0xb5, 0x28, 0x22, 0xa6,
+    0x26, 0x29, 0x52, 0x45, 0xba, 0x39, 0x2a, 0xeb,
+    0x83, 0x4d, 0xc8, 0xe3, 0x32, 0x04, 0x34, 0xc1,
+    0x5a, 0xd0, 0x94, 0x76, 0xf8, 0xc9, 0xb5, 0x5b,
+    0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0b,
+    0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e,
+    0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfe, 0x0a, 0x00,
+    0x3c, 0x03, 0x00, 0x20, 0x00, 0x20, 0x7d, 0x01,
+    0xa0, 0x07, 0x62, 0x24, 0x25, 0xfc, 0xf2, 0x0b,
+    0xfc, 0x9d, 0x8a, 0x46, 0x55, 0x7e, 0x41, 0xa4,
+    0x60, 0x5a, 0xf7, 0x24, 0xf4, 0x3d, 0xa7, 0x37,
+    0x2b, 0x0c, 0x7a, 0x8b, 0xc5, 0x11, 0x00, 0x04,
+    0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b,
+    0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e,
+    0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfe, 0x0d, 0x00,
+    0x3a, 0x43, 0x00, 0x20, 0x00, 0x20, 0xdc, 0x64,
+    0x80, 0x31, 0xec, 0xe7, 0x0e, 0x89, 0x0b, 0xa4,
+    0xf7, 0x45, 0x55, 0xef, 0xf0, 0xce, 0xe2, 0x51,
+    0x50, 0xaf, 0x92, 0x37, 0x67, 0x0f, 0x6d, 0x55,
+    0xb2, 0xbc, 0x30, 0x6c, 0x35, 0x76, 0x00, 0x04,
+    0x00, 0x01, 0x00, 0x01, 0x00, 0x0b, 0x65, 0x78,
+    0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f,
+    0x6d, 0x00, 0x00
+};
+
+/* base64(ECHConfigList) with corrupt ciphersuite length and public_name */
+static const char b64_bad_cs[] =
+    "AD7+DQA6uAAgACAogff+HZbirYdQCfXI01GBPP8AEKYyK/D/0DoeXD84fgAQAAE"
+    "AAQgLZXhhbUNwbGUuYwYAAAAAQwA=";
+
+/* An ECHConfigList with one ECHConfig but of the wrong version */
+static const unsigned char bin_bad_ver[] = {
+    0x00, 0x3e, 0xfe, 0xff, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/*
+ * An ECHConflgList with 2 ECHConfig values that are both
+ * of the wrong version. The versions here are 0xfe03 (we
+ * currently support only 0xfe0d)
+ */
+static const unsigned char bin_bad_ver2[] = {
+    0x00, 0x80, 0xfe, 0x03, 0x00, 0x3c, 0x00, 0x00,
+    0x20, 0x00, 0x20, 0x71, 0xa5, 0xe0, 0xb4, 0x6d,
+    0xdf, 0xa4, 0xda, 0xed, 0x69, 0xa5, 0xc7, 0x8b,
+    0x9d, 0xa5, 0x13, 0x0c, 0x36, 0x83, 0x7a, 0x03,
+    0x72, 0x1d, 0xf6, 0x1e, 0xc5, 0x83, 0x1a, 0x11,
+    0x73, 0xce, 0x2d, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x31,
+    0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+    0x00, 0x00, 0xfe, 0x03, 0x00, 0x3c, 0x00, 0x00,
+    0x20, 0x00, 0x20, 0x69, 0x88, 0xfd, 0x8f, 0xc9,
+    0x0b, 0xb7, 0x2d, 0x96, 0x6d, 0xe0, 0x22, 0xf0,
+    0xc8, 0x1b, 0x62, 0x2b, 0x1c, 0x94, 0x96, 0xad,
+    0xef, 0x55, 0xdb, 0x9f, 0xeb, 0x0d, 0xa1, 0x4b,
+    0x0c, 0xd7, 0x36, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x32,
+    0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+    0x00, 0x00
+};
+
+/*
+ * An ECHConfigList with one ECHConfig with an all-zero public value.
+ * That should be ok, for 25519, but hey, just in case:-)
+ */
+static const unsigned char bin_zero[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/*
+ * The next set of samples are syntactically invalid
+ * Proper fuzzing is still needed but no harm having
+ * these too. Generally these are bad version of
+ * our nominal encoding with some octet(s) replaced
+ * by 0xFF values. Other hex letters are lowercase
+ * so you can find the altered octet(s).
+ */
+
+/* wrong overall length (replacing 0x3e with 0xFF) */
+static const unsigned char bin_bad_olen[] = {
+    0x00, 0xFF, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0xFF, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong ECHConfig inner length (replacing 0x3a with 0xFF) */
+static const unsigned char bin_bad_ilen[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0xFF, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong length for public key (replaced 0x20 with 0xFF) */
+static const unsigned char bin_bad_pklen[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0xFF, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong length for ciphersuites (replaced 0x04 with 0xFF) */
+static const unsigned char bin_bad_cslen[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0xFF, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong length for public name (replaced 0x0b with 0xFF) */
+static const unsigned char bin_bad_pnlen[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0xFF, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* non-zero extension length (0xFF at end) but no extension value */
+static const unsigned char bin_bad_extlen[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0xFF
+};
+
+/*
+ * The next set have bad kem, kdf or aead values - this time with
+ * 0xAA as the replacement value
+ */
+
+/* wrong KEM ID (replaced 0x20 with 0xAA) */
+static const unsigned char bin_bad_kemid[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0xAA, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong KDF ID (replaced 0x01 with 0xAA) */
+static const unsigned char bin_bad_kdfid[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0xAA, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* wrong AEAD ID (replaced 0x01 with 0xAA) */
+static const unsigned char bin_bad_aeadid[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0xAA, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* ECHConfig supports two symmetric suites */
+static const unsigned char bin_multi_suite[] = {
+    0x00, 0x42, 0xfe, 0x0d, 0x00, 0x3e, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x08, 0x00, 0x01, 0x00,
+    0x01,
+    0x00, 0x02, 0x00, 0x02,
+    0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/*
+ * sorta wrong AEAD ID; replaced 0x0001 with 0xFFFF
+ * which is the export only pseudo-aead-id - that
+ * should not work in our test, same as the others,
+ * but worth a specific test, as it'll fail in a
+ * different manner
+ */
+static const unsigned char bin_bad_aeadid_ff[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0xFF,
+    0xFF, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/*
+ * An ECHConfigList with a bad ECHConfig
+ * (aead is 0xFFFF), followed by a good
+ * one.
+ */
+static const unsigned char bin_bad_then_good[] = {
+    0x00, 0x7c, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0xFF,
+    0xFF, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00,
+    0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, 0x20, 0x00,
+    0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, 0xc5, 0xfe,
+    0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, 0xa4, 0x33,
+    0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, 0x5a, 0x42,
+    0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, 0x60, 0x16,
+    0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00,
+    0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+    0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* couple of harmless extensions */
+static const unsigned char bin_ok_exts[] = {
+    0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09,
+    0x0a, 0x0b, 0x00, 0x00, 0x0c, 0x0d, 0x00, 0x01,
+    0x02
+};
+
+/* one "mandatory" extension (high bit of type set) */
+static const unsigned char bin_mand_ext[] = {
+    0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09,
+    0x0a, 0x0b, 0x00, 0x00, 0xFc, 0x0d, 0x00, 0x01,
+    0x02
+};
+
+/* extension with bad length (0xFFFF) */
+static const unsigned char bin_bad_inner_extlen[] = {
+    0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09,
+    0x0a, 0x0b, 0x00, 0x00, 0x0c, 0x0d, 0x00, 0xFF,
+    0x02
+};
+
+/* good, other than a NUL inside the public_name */
+static const unsigned char bin_nul_in_pn[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x00, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* good, other than a dot at the end of the public_name */
+static const unsigned char bin_pn_dot_at_end[] = {
+    0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x2e, 0x00, 0x00
+};
+
+/*
+ * An ECHConfigList with a good ECHConfig followed by a bad
+ * one with the 1st internal length (0xFFFF) too big
+ */
+static const unsigned char bin_good_then_bad[] = {
+    0x00, 0x7c, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00,
+    0xfe, 0x0d, 0xFF, 0xFF, 0xbb, 0x00, 0x20, 0x00,
+    0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, 0xc5, 0xfe,
+    0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, 0xa4, 0x33,
+    0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, 0x5a, 0x42,
+    0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, 0x60, 0x16,
+    0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00,
+    0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+    0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00
+};
+
+/* generally very short:-) */
+static const unsigned char bin_short[] = {
+    0x00, 0x05, 0xfe, 0x0d, 0x00, 0x01, 0x01
+};
+
+/* kind of an empty value */
+static const unsigned char bin_empty[] = {
+    0x00, 0x00
+};
+
+/*
+ * An ECHConfigList with an unsupported ECHConfig and
+ * that's too short.
+ */
+static const unsigned char bin_ver_short[] = {
+    0x00, 0x3e, 0xfe, 0xFF, 0x00, 0x3a, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
+};
+
+/*
+ * too-long extension - OSSL_ECH_MAX_ECHCONFIGEXT_LEN is
+ * 512, this is 513 (0x0201), end of the 8-th line
+ * */
+static const unsigned char bin_long_ext[] = {
+    0x02, 0x43, 0xfe, 0x0d, 0x02, 0x3f, 0xbb, 0x00,
+    0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2,
+    0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c,
+    0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e,
+    0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73,
+    0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00,
+    0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70,
+    0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x05,
+    0xFF, 0xFF, 0x02, 0x01,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x00
+};
+
+/* struct for ingest test vector and results */
+typedef struct INGEST_TV_T {
+    char *name; /* name for verbose output */
+    const unsigned char *tv; /* test vector */
+    size_t len; /* len(tv) - sizeof(tv) if binary, subtract 1 for strings */
+    int pemenc; /* whether PEM encoded (1) or not (0) */
+    int read; /* result expected from read function on tv */
+    int keysb4; /* the number of private keys expected before downselect */
+    int entsb4; /* the number of public keys b4 */
+    int index; /* the index to use for downselect */
+    int expected; /* the result expected from a downselect */
+    int keysaftr; /* the number of keys expected after downselect */
+    int entsaftr; /* the number of public keys after */
+} ingest_tv_t;
+
+static ingest_tv_t ingest_tvs[] = {
+    /* PEM test vectors */
+    { "PEM basic/last", (unsigned char *)pem_kp1, sizeof(pem_kp1) - 1,
+      1, 1, 1, 1, OSSL_ECHSTORE_LAST, 1, 1, 1 },
+    { "PEM basic/0", (unsigned char *)pem_pk1, sizeof(pem_pk1) - 1,
+      1, 1, 0, 1, 0, 1, 0, 1 },
+    { "PEM basic/2nd", (unsigned char *)pem_pk1, sizeof(pem_pk1) - 1,
+      1, 1, 0, 1, 2, 0, 0, 1 },
+    { "ECDSA priv + 25519 pub", (unsigned char *)pem_mismatch_priv,
+      sizeof(pem_mismatch_priv) - 1,
+      1, 0, 0, 0, 0, 0, 0, 0 },
+    { "PEM string typo", (unsigned char *)pem_typo, sizeof(pem_typo) - 1,
+      1, 0, 0, 0, 0, 0, 0, 0 },
+    /* downselect from the 2, at each position */
+    { "PEM 4->2/0", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1,
+      1, 1, 0, 2, 0, 1, 0, 1 },
+    { "PEM 4->2/1", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1,
+      1, 1, 0, 2, 1, 1, 0, 1 },
+    /* in the next one below, downselect fails, so we still have 2 entries */
+    { "PEM 4->2/2", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1,
+      1, 1, 0, 2, 3, 0, 0, 2 },
+    /* b64 test vectors */
+    { "B64 basic/last", (unsigned char *)b64_pk1, sizeof(b64_pk1) - 1,
+      0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 },
+    { "B64 6->3/2", (unsigned char *)b64_6_to_3, sizeof(b64_6_to_3) - 1,
+      0, 1, 0, 3, 2, 1, 0, 1 },
+    { "B64 bad suitelen", (unsigned char *)b64_bad_cs, sizeof(b64_bad_cs) - 1,
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    /* binary test vectors */
+    { "bin 6->3/2", (unsigned char *)bin_6_to_3, sizeof(bin_6_to_3),
+      0, 1, 0, 3, 2, 1, 0, 1 },
+    { "bin 2 symm suites", (unsigned char *)bin_multi_suite,
+      sizeof(bin_multi_suite),
+      0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 },
+    { "bin all-zero pub", (unsigned char *)bin_zero, sizeof(bin_zero),
+      0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 },
+    { "bin ok exts", (unsigned char *)bin_ok_exts, sizeof(bin_ok_exts),
+      0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 },
+    { "bin bad ver", (unsigned char *)bin_bad_ver, sizeof(bin_bad_ver),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin 2 bad ver", (unsigned char *)bin_bad_ver2, sizeof(bin_bad_ver2),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad len", (unsigned char *)bin_bad_olen, sizeof(bin_bad_olen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad inner len", (unsigned char *)bin_bad_ilen, sizeof(bin_bad_ilen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad pk len", (unsigned char *)bin_bad_pklen, sizeof(bin_bad_pklen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad suitelen", (unsigned char *)bin_bad_cslen, sizeof(bin_bad_cslen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad pn len", (unsigned char *)bin_bad_pnlen, sizeof(bin_bad_pnlen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad extlen", (unsigned char *)bin_bad_extlen, sizeof(bin_bad_extlen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad kemid", (unsigned char *)bin_bad_kemid, sizeof(bin_bad_kemid),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad kdfid", (unsigned char *)bin_bad_kdfid, sizeof(bin_bad_kdfid),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad aeadid", (unsigned char *)bin_bad_aeadid, sizeof(bin_bad_aeadid),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin exp aeadid", (unsigned char *)bin_bad_aeadid_ff,
+      sizeof(bin_bad_aeadid_ff),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad,good", (unsigned char *)bin_bad_then_good,
+      sizeof(bin_bad_then_good),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin mand ext", (unsigned char *)bin_mand_ext, sizeof(bin_mand_ext),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin bad inner extlen", (unsigned char *)bin_bad_inner_extlen,
+      sizeof(bin_bad_inner_extlen),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin NUL in PN", (unsigned char *)bin_nul_in_pn, sizeof(bin_nul_in_pn),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin PN ends in dot", (unsigned char *)bin_pn_dot_at_end,
+      sizeof(bin_pn_dot_at_end),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin short", (unsigned char *)bin_short, sizeof(bin_short),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin empty", (unsigned char *)bin_empty, sizeof(bin_empty),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin ver short", (unsigned char *)bin_ver_short, sizeof(bin_ver_short),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin long ext", (unsigned char *)bin_long_ext, sizeof(bin_long_ext),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+    { "bin good then bad", (unsigned char *)bin_good_then_bad,
+      sizeof(bin_good_then_bad),
+      0, 0, 0, 0, 0, 0, 0, 0 },
+};
+
+/* similar, but slightly simpler setup for file reading tests */
+typedef struct FNT_T {
+    char *fname; /* relative file name */
+    int read; /* expected result from a pem_read of that */
+} 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 },
+};
+
+/* string from which we construct varieties of HPKE suite */
+static const char *kem_str_list[] = {
+    "P-256", "P-384", "P-521", "x25519", "x448",
+};
+static const char *kdf_str_list[] = {
+    "hkdf-sha256", "hkdf-sha384", "hkdf-sha512",
+};
+static const char *aead_str_list[] = {
+    "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305",
+};
+
+typedef enum OPTION_choice {
+    OPT_ERR = -1,
+    OPT_EOF = 0,
+    OPT_VERBOSE,
+    OPT_TEST_ENUM
+} OPTION_CHOICE;
+
+const OPTIONS *test_get_options(void)
+{
+    static const OPTIONS test_options[] = {
+        OPT_TEST_OPTIONS_DEFAULT_USAGE,
+        { "v", OPT_VERBOSE, '-', "Enable verbose mode" },
+        { OPT_HELP_STR, 1, '-', "Run ECH tests\n" },
+        { NULL }
+    };
+    return test_options;
+}
+
+/*
+ * For the relevant test vector in our array above:
+ * - try decode
+ * - if not expected to decode, we're done
+ * - check we got the right number of keys/ECHConfig values
+ * - do some calls with getting info, downselecting etc. and
+ *   check results as expected
+ * - do a write_pem call on the results
+ * - flush keys 'till now and check they're all gone
+ */
+static int ech_ingest_test(int run)
+{
+    OSSL_ECHSTORE *es = NULL;
+    BIO *in = NULL, *out = NULL;
+    int i, rv = 0, keysb4, keysaftr, actual_ents = 0, has_priv, for_retry;
+    ingest_tv_t *tv = &ingest_tvs[run];
+    time_t secs = 0, add_time = 0, flush_time = 0;
+    char *pn = NULL, *ec = NULL;
+
+    if ((in = BIO_new(BIO_s_mem())) == NULL
+        || BIO_write(in, tv->tv, (int)tv->len) <= 0
+        || (out = BIO_new(BIO_s_mem())) == NULL
+        || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL)
+        goto end;
+    if (verbose)
+        TEST_info("Iteration: %d %s", run + 1, tv->name);
+    /* just in case of bad edits to table */
+    if (tv->pemenc != 1 && tv->pemenc != 0) {
+        TEST_info("Bad test vector entry");
+        goto end;
+    }
+    add_time = time(0);
+    if (tv->pemenc == 1
+        && !TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY),
+                        tv->read))
+        goto end;
+    if (tv->pemenc != 1
+        && !TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, in), tv->read))
+        goto end;
+    /* if we provided a deliberately bad tv then we're done */
+    if (tv->read != 1) {
+        rv = 1;
+        goto end;
+    }
+    if (!TEST_true(OSSL_ECHSTORE_num_keys(es, &keysb4))
+        || !TEST_true(OSSL_ECHSTORE_num_entries(es, &actual_ents))
+        || !TEST_int_eq(keysb4, tv->keysb4)
+        || !TEST_int_eq(actual_ents, tv->entsb4)
+        || !TEST_int_eq(OSSL_ECHSTORE_get1_info(es, -1, &secs, &pn, &ec,
+                                                &has_priv, &for_retry), 0))
+        goto end;
+    OPENSSL_free(pn);
+    pn = NULL;
+    OPENSSL_free(ec);
+    ec = NULL;
+    for (i = 0; i != actual_ents; i++) {
+        if (!TEST_true(OSSL_ECHSTORE_get1_info(es, i, &secs, &pn, &ec,
+                                               &has_priv, &for_retry)))
+            goto end;
+        OPENSSL_free(pn);
+        pn = NULL;
+        OPENSSL_free(ec);
+        ec = NULL;
+    }
+    /* ensure silly index fails ok */
+    if (!TEST_false(OSSL_ECHSTORE_downselect(es, -20))
+        || !TEST_int_eq(OSSL_ECHSTORE_downselect(es, tv->index), tv->expected)
+        || !TEST_true(OSSL_ECHSTORE_num_keys(es, &keysaftr))
+        || !TEST_int_eq(keysaftr, tv->keysaftr)
+        || !TEST_true(OSSL_ECHSTORE_num_entries(es, &actual_ents))
+        || !TEST_int_eq(actual_ents, tv->entsaftr)
+        || !TEST_true(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_LAST, out))
+        || !TEST_true(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_ALL, out))
+        || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, out)))
+        goto end;
+    flush_time = time(0);
+    /*
+     * Occasionally, flush_time will be 1 more than add_time. We'll
+     * check for that as that should catch a few more code paths
+     * in the flush_keys API.
+     * When flush_time is 1 more, we may or may not have flushed
+     * the one and only key (depending on which "side" of the second
+     * it was generated, so we may be left with 0 or 1 keys.
+     */
+    if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, flush_time - add_time))
+        || !TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1)
+        || ((flush_time <= add_time) && !TEST_int_eq(keysaftr, 0))
+        || ((flush_time > add_time) && !TEST_int_eq(keysaftr, 1)
+            && !TEST_int_eq(keysaftr, 0))) {
+        TEST_info("Flush time: %lld, add_time: %lld", (long long)flush_time,
+                  (long long)add_time);
+        goto end;
+    }
+    rv = 1;
+end:
+    OPENSSL_free(pn);
+    OPENSSL_free(ec);
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(in);
+    BIO_free_all(out);
+    return rv;
+}
+
+/* make a bunch of calls with bad, mostly NULL, arguments */
+static int ech_store_null_calls(void)
+{
+    int rv = 0, count = 0, has_priv, for_retry;
+    OSSL_ECHSTORE *es = OSSL_ECHSTORE_new(NULL, NULL);
+    OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+    BIO *inout = BIO_new(BIO_s_mem());
+    EVP_PKEY *priv = EVP_PKEY_new();
+    time_t secs;
+    char *pn = NULL, *ec = NULL;
+
+    OSSL_ECHSTORE_free(NULL);
+    if (!TEST_false(OSSL_ECHSTORE_new_config(NULL, OSSL_ECH_CURRENT_VERSION,
+                                             0, "example.com", hpke_suite))
+        || !TEST_false(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION,
+                                                0, NULL, hpke_suite))
+        || !TEST_false(OSSL_ECHSTORE_new_config(es, 0xffff, 0,
+                                                "example.com", hpke_suite)))
+        goto end;
+    hpke_suite.kdf_id = 0xAAAA; /* a bad value */
+    if (!TEST_false(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION,
+                                             0, "example.com", hpke_suite))
+        || !TEST_false(OSSL_ECHSTORE_write_pem(NULL, 0, inout))
+        || !TEST_false(OSSL_ECHSTORE_write_pem(es, 0, NULL))
+        || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, inout))
+        || !TEST_false(OSSL_ECHSTORE_read_echconfiglist(NULL, inout))
+        || !TEST_false(OSSL_ECHSTORE_read_echconfiglist(es, NULL))
+        || !TEST_false(OSSL_ECHSTORE_get1_info(NULL, 0, &secs, &pn, &ec,
+                                               &has_priv, &for_retry))
+        || !TEST_false(OSSL_ECHSTORE_downselect(NULL, 0))
+        || !TEST_false(OSSL_ECHSTORE_downselect(es, 100))
+        || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(NULL, priv,
+                                                           inout, 0))
+        || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, NULL, inout, 0))
+        || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, NULL, 0))
+        || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv,
+                                                           inout, 100))
+        /* this one fails 'cause priv has no real value, even if non NULL */
+        || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, inout,
+                                                           OSSL_ECH_NO_RETRY))
+        || !TEST_false(OSSL_ECHSTORE_read_pem(NULL, inout, OSSL_ECH_NO_RETRY))
+        || !TEST_false(OSSL_ECHSTORE_read_pem(es, NULL, OSSL_ECH_NO_RETRY))
+        || !TEST_false(OSSL_ECHSTORE_read_pem(es, inout, 100))
+        || !TEST_false(OSSL_ECHSTORE_num_keys(NULL, &count))
+        || !TEST_false(OSSL_ECHSTORE_num_keys(es, NULL))
+        || !TEST_false(OSSL_ECHSTORE_flush_keys(NULL, 0))
+        || !TEST_false(OSSL_ECHSTORE_flush_keys(es, -1))
+        || !TEST_false(OSSL_ECHSTORE_num_entries(es, NULL)))
+        goto end;
+    rv = 1;
+end:
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(inout);
+    EVP_PKEY_free(priv);
+    return rv;
+}
+
+/* read some files, some that work, some that fail */
+static int ech_test_file_read(int run)
+{
+    int rv = 0;
+    OSSL_ECHSTORE *es = NULL;
+    BIO *in = NULL;
+    fnt_t *ft = &fnames[run];
+    char *fullname = NULL;
+    size_t fnlen = 0;
+
+    es = OSSL_ECHSTORE_new(NULL, NULL);
+    if (es == NULL)
+        goto end;
+    fnlen = strlen(certsdir) + 1 + strlen(ft->fname) + 1;
+    fullname = OPENSSL_malloc(fnlen);
+    if (fullname == NULL)
+        goto end;
+    snprintf(fullname, fnlen, "%s/%s", certsdir, ft->fname);
+    if (verbose)
+        TEST_info("testing read of %s", fullname);
+    in = BIO_new_file(fullname, "r");
+    if (in == NULL) {
+        TEST_info("BIO_new_file failed for %s", ft->fname);
+        goto end;
+    }
+    if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY),
+                     ft->read))
+        goto end;
+    rv = 1;
+end:
+    OPENSSL_free(fullname);
+    OSSL_ECHSTORE_free(es);
+    BIO_free_all(in);
+    return rv;
+}
+
+/* calls with bad, NULL, and simple, arguments, for generic code coverage  */
+static int ech_api_basic_calls(void)
+{
+    int rv = 0;
+    SSL_CTX *ctx = NULL;
+    SSL *s = NULL;
+    OSSL_ECHSTORE *es = NULL, *es1 = NULL;
+    char *rinner, *inner = "inner.example.com";
+    char *router, *outer = "example.com";
+    unsigned char alpns[] = { 'h', '2' };
+    size_t alpns_len = sizeof(alpns);
+    char *gsuite = "X25519,hkdf-sha256,aes-256-gcm";
+    uint16_t gtype = 0xfe09;
+    unsigned char *rc = NULL;
+    size_t rclen = 0;
+    BIO *in = NULL;
+
+    /* NULL args */
+    if (!TEST_false(SSL_CTX_set1_echstore(NULL, NULL))
+        || !TEST_false(SSL_set1_echstore(NULL, NULL))
+        || !TEST_ptr_eq(SSL_CTX_get1_echstore(NULL), NULL)
+        || !TEST_ptr_eq(SSL_get1_echstore(NULL), NULL)
+        || !TEST_false(SSL_ech_set1_server_names(NULL, NULL, NULL, -1))
+        || !TEST_false(SSL_ech_set1_outer_server_name(NULL, NULL, -1))
+        || !TEST_false(SSL_CTX_ech_set1_outer_alpn_protos(NULL, NULL, -1))
+        || !TEST_false(SSL_ech_set1_outer_alpn_protos(NULL, NULL, -1))
+        || !TEST_false(SSL_ech_set1_grease_suite(NULL, NULL))
+        || !TEST_false(SSL_ech_set_grease_type(NULL, 0)))
+        goto end;
+    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;
+
+    /* add an ECHConfigList with extensions to exercise init code */
+    if (!TEST_ptr(es = OSSL_ECHSTORE_new(NULL, NULL))
+        || !TEST_ptr(in = BIO_new(BIO_s_mem()))
+        || !TEST_int_gt(BIO_write(in, bin_ok_exts, sizeof(bin_ok_exts)), 0)
+        || !TEST_true(OSSL_ECHSTORE_read_echconfiglist(es, in))
+        || !TEST_ptr(ctx = SSL_CTX_new_ex(NULL, NULL, TLS_server_method())))
+        goto end;
+    /* check status of SSL connection before OSSL_ECHSTORE set */
+    if (!TEST_ptr(s = SSL_new(ctx))
+        || !TEST_int_eq(SSL_ech_get1_status(s, NULL, NULL),
+                        SSL_ECH_STATUS_FAILED)
+        || !TEST_int_eq(SSL_ech_get1_status(s, &rinner, &router),
+                        SSL_ECH_STATUS_NOT_CONFIGURED))
+        goto end;
+    SSL_set_options(s, SSL_OP_ECH_GREASE);
+    if (!TEST_int_eq(SSL_ech_get1_status(s, &rinner, &router),
+                     SSL_ECH_STATUS_GREASE))
+        goto end;
+    SSL_free(s);
+    s = NULL; /* for some other tests */
+    if (!TEST_true(SSL_CTX_set1_echstore(ctx, es)))
+        goto end;
+    if (!TEST_ptr((es1 = SSL_CTX_get1_echstore(ctx))))
+        goto end;
+    OSSL_ECHSTORE_free(es1);
+    es1 = NULL;
+    if (!TEST_false(SSL_set1_echstore(s, es)))
+        goto end;
+    /* do this one before SSL_new to exercise a bit of init code */
+    if (!TEST_true(SSL_CTX_ech_set1_outer_alpn_protos(ctx, alpns, alpns_len)))
+        goto end;
+    s = SSL_new(ctx);
+    if (!TEST_true(SSL_set1_echstore(s, es)))
+        goto end;
+    if (!TEST_ptr(es1 = SSL_get1_echstore(s)))
+        goto end;
+    OSSL_ECHSTORE_free(es1);
+    es1 = NULL;
+    if (!TEST_true(SSL_ech_set1_server_names(s, inner, outer, 0))
+        || !TEST_true(SSL_ech_set1_outer_server_name(s, outer, 0))
+        || !TEST_true(SSL_ech_set1_outer_alpn_protos(s, alpns, alpns_len))
+        || !TEST_true(SSL_ech_set1_grease_suite(s, gsuite))
+        || !TEST_true(SSL_ech_set_grease_type(s, gtype))
+        || !TEST_true(SSL_ech_get1_retry_config(s, &rc, &rclen))
+        || !TEST_false(rclen)
+        || !TEST_ptr_eq(rc, NULL))
+        goto end;
+    SSL_CTX_ech_set_callback(ctx, ech_test_cb);
+    SSL_ech_set_callback(s, ech_test_cb);
+
+    /* all good */
+    rv = 1;
+end:
+    BIO_free_all(in);
+    OSSL_ECHSTORE_free(es1);
+    OSSL_ECHSTORE_free(es);
+    SSL_CTX_free(ctx);
+    SSL_free(s);
+    return rv;
+}
+
+/*
+ * Test boringssl compatibility API. We don't need exhaustive
+ * tests here as this is a simple enough wrapper on things
+ * tested elsewhere.
+ */
+static int ech_boring_compat(void)
+{
+    int rv = 0;
+    SSL_CTX *ctx = NULL;
+    SSL *s = NULL;
+
+    if (!TEST_false(SSL_set1_ech_config_list(NULL, NULL, 0))
+        || !TEST_ptr(ctx = SSL_CTX_new_ex(NULL, NULL, TLS_server_method()))
+        || !TEST_ptr(s = SSL_new(ctx))
+        || !TEST_true(SSL_set1_ech_config_list(s, NULL, 0))
+        || !TEST_true(SSL_set1_ech_config_list(s, (uint8_t *)b64_pk1,
+                                               sizeof(b64_pk1) - 1))
+        || !TEST_true(SSL_set1_ech_config_list(s, (uint8_t *)bin_6_to_3,
+                                               sizeof(bin_6_to_3)))
+        /* test a fail */
+        || !TEST_false(SSL_set1_ech_config_list(s, (uint8_t *)b64_pk1,
+                                                sizeof(b64_pk1) - 2)))
+        goto end;
+    rv = 1;
+end:
+    SSL_CTX_free(ctx);
+    SSL_free(s);
+    return rv;
+}
+
+/* values that can be used in helper below */
+# define OSSL_ECH_TEST_BASIC    0
+# define OSSL_ECH_TEST_HRR      1
+# define OSSL_ECH_TEST_EARLY    2
+# define OSSL_ECH_TEST_CUSTOM   3
+# 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 */
+/* note: early-data is prohibited after HRR so no tests for that */
+
+/*
+ * @brief ECH roundtrip test helper
+ * @param idx specifies which ciphersuite
+ * @araam combo specifies which particular test we want to roundtrip
+ * @return 1 for good, 0 for bad
+ *
+ * The idx input here is from 0..44 and is broken down into a
+ * kem, kdf and aead. If you run in verbose more ("-v") then
+ * there'll be a "Doing: ..." trace line that says which suite
+ * is being tested in string form.
+ *
+ * The combo input is one of the #define'd OSSL_ECH_TEST_*
+ * values above.
+ */
+static int test_ech_roundtrip_helper(int idx, int combo)
+{
+    int res = 0, kemind, kdfind, aeadind, kemsz, kdfsz, aeadsz;
+    int clientstatus, serverstatus, server = 1, client = 0;
+    unsigned int context;
+    OSSL_ECHSTORE *es = NULL;
+    OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
+    uint16_t ech_version = OSSL_ECH_CURRENT_VERSION;
+    uint8_t max_name_length = 0;
+    char *public_name = "example.com", suitestr[100];
+    SSL_CTX *cctx = NULL, *sctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL;
+    char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL;
+    SSL_SESSION *sess = NULL;
+    size_t written = 0, readbytes = 0;
+    unsigned char ed[21], buf[1024];
+
+    /* split idx into kemind, kdfind, aeadind */
+    kemsz = OSSL_NELEM(kem_str_list);
+    kdfsz = OSSL_NELEM(kdf_str_list);
+    aeadsz = OSSL_NELEM(aead_str_list);
+    kemind = (idx / (kdfsz * aeadsz)) % kemsz;
+    kdfind = (idx / aeadsz) % kdfsz;
+    aeadind = idx % aeadsz;
+    /* initialise early data stuff, just in case */
+    memset(ed, 'A', sizeof(ed));
+    snprintf(suitestr, 100, "%s,%s,%s", kem_str_list[kemind],
+             kdf_str_list[kdfind], aead_str_list[aeadind]);
+    if (verbose)
+        TEST_info("Doing: iter: %d, suite: %s", idx, suitestr);
+    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,
+                                               public_name, hpke_suite))
+        || !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 (combo == OSSL_ECH_TEST_V12) {
+        /* force client to TLSv1.2 and later fail as expected */
+        if (!TEST_true(SSL_CTX_set_max_proto_version(cctx, TLS1_2_VERSION)))
+            goto end;
+        if (!TEST_true(SSL_CTX_set_min_proto_version(cctx, TLS1_2_VERSION)))
+            goto end;
+    }
+    if (combo == OSSL_ECH_TEST_EARLY || combo == OSSL_ECH_TEST_ENOE) {
+        if (!TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_ANTI_REPLAY))
+            || !TEST_true(SSL_CTX_set_max_early_data(sctx,
+                                                     SSL3_RT_MAX_PLAIN_LENGTH))
+            || !TEST_true(SSL_CTX_set_recv_max_early_data(sctx,
+                                                          SSL3_RT_MAX_PLAIN_LENGTH)))
+            goto end;
+    }
+    if (combo == OSSL_ECH_TEST_CUSTOM) {
+        context = SSL_EXT_CLIENT_HELLO; /* add custom CH ext to client/server */
+        if (!TEST_true(SSL_CTX_add_custom_ext(cctx, TEST_EXT_TYPE1, context,
+                                              new_add_cb, new_free_cb,
+                                              &client, new_parse_cb, &client))
+            || !TEST_true(SSL_CTX_add_custom_ext(sctx, TEST_EXT_TYPE1, context,
+                                                 new_add_cb, new_free_cb,
+                                                 &server, new_parse_cb, &server))
+            || !TEST_true(SSL_CTX_add_custom_ext(cctx, TEST_EXT_TYPE2, context,
+                                                 new_add_cb, NULL,
+                                                 &client, NULL, &client))
+            || !TEST_true(SSL_CTX_add_custom_ext(sctx, TEST_EXT_TYPE2, context,
+                                                 new_add_cb, NULL,
+                                                 &server, NULL, &server)))
+            goto end;
+    }
+    if (combo == OSSL_ECH_TEST_CBS) {
+        SSL_CTX_ech_set_callback(sctx, ech_test_cb);
+        SSL_CTX_set_client_hello_cb(sctx, ch_test_cb, NULL);
+    }
+    if (combo != OSSL_ECH_TEST_ENOE
+        && !TEST_true(SSL_CTX_set1_echstore(cctx, es)))
+        goto end;
+    if (!TEST_true(SSL_CTX_set1_echstore(sctx, es))
+        || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl,
+                                         &clientssl, NULL, NULL)))
+        goto end;
+    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")))
+        goto end;
+    if (combo == OSSL_ECH_TEST_V12) {
+        if (!TEST_false(create_ssl_connection(serverssl, clientssl,
+                                              SSL_ERROR_NONE)))
+            goto end;
+        res = 1;
+        goto end;
+    } else {
+        if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+                                             SSL_ERROR_NONE)))
+            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);
+    serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+    if (verbose)
+        TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
+    if (combo != OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
+        goto end;
+    if (combo == OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_NOT_TRIED))
+        goto end;
+    if (combo != OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+        goto end;
+    if (combo == OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED))
+        goto end;
+    if (combo == OSSL_ECH_TEST_CBS && !TEST_int_eq(ch_test_cb_ok, 1))
+        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) {
+        res = 1;
+        goto end;
+    }
+    /* continue for EARLY test */
+    if (combo != OSSL_ECH_TEST_EARLY && combo != OSSL_ECH_TEST_ENOE)
+        goto end;
+    /* shutdown for start over */
+    sess = SSL_get1_session(clientssl);
+    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))
+        || !TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example"))
+        || !TEST_true(SSL_set_session(clientssl, sess))
+        || !TEST_true(SSL_write_early_data(clientssl, ed, sizeof(ed), &written))
+        || !TEST_size_t_eq(written, sizeof(ed))
+        || !TEST_int_eq(SSL_read_early_data(serverssl, buf, sizeof(buf),
+                                            &readbytes),
+                        SSL_READ_EARLY_DATA_SUCCESS)
+        || !TEST_size_t_eq(written, readbytes))
+        goto end;
+    /*
+     * Server should be able to write data, and client should be able to
+     * read it.
+     */
+    if (!TEST_true(SSL_write_early_data(serverssl, ed, sizeof(ed), &written))
+            || !TEST_size_t_eq(written, sizeof(ed))
+            || !TEST_true(SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes))
+            || !TEST_mem_eq(buf, readbytes, ed, sizeof(ed)))
+        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);
+    serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
+    if (verbose)
+        TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
+    if (combo != OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
+        goto end;
+    if (combo == OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_NOT_TRIED))
+        goto end;
+    if (combo != OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+        goto end;
+    if (combo == OSSL_ECH_TEST_ENOE
+        && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED))
+        goto end;
+    /* all good */
+    res = 1;
+end:
+    OSSL_ECHSTORE_free(es);
+    OPENSSL_free(sinner);
+    OPENSSL_free(souter);
+    OPENSSL_free(cinner);
+    OPENSSL_free(couter);
+    SSL_SESSION_free(sess);
+    SSL_free(clientssl);
+    SSL_free(serverssl);
+    SSL_CTX_free(cctx);
+    SSL_CTX_free(sctx);
+    ch_test_cb_ok = 0;
+    return res;
+}
+
+/* Test roundtrip with ECH for any suite */
+static int test_ech_suites(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: test_ech_suites");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_BASIC);
+}
+
+/* ECH with HRR for the given suite */
+static int test_ech_hrr(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: test_ech_hrr");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_HRR);
+}
+
+/* ECH with early data for the given suite */
+static int test_ech_early(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: test_ech_early");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_EARLY);
+}
+
+/* Test a roundtrip with ECH, and a custom CH extension */
+static int ech_custom_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech_custom_test");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CUSTOM);
+}
+
+/* Test a roundtrip with No ECH, and early data */
+static int ech_enoe_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech_no ech + early test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_ENOE);
+}
+
+/* Test a roundtrip with ECH, and callbacks */
+static int ech_cb_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech + callbacks test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CBS);
+}
+
+/* Test a roundtrip (fails) with ECH but a TLSv1.2 SSL_CTX */
+static int ech_v12_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech TLSv1.2 test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_V12);
+}
+
+#endif
+
+int setup_tests(void)
+{
+#ifndef OPENSSL_NO_ECH
+    OPTION_CHOICE o;
+    int suite_combos;
+
+    while ((o = opt_next()) != OPT_EOF) {
+        switch (o) {
+        case OPT_VERBOSE:
+            verbose = 1;
+            break;
+        case OPT_TEST_CASES:
+            break;
+        default:
+            return 0;
+        }
+    }
+    certsdir = test_get_argument(0);
+    if (certsdir == NULL)
+        certsdir = DEF_CERTS_DIR;
+    cert = test_mk_file_path(certsdir, "echserver.pem");
+    if (cert == NULL)
+        goto err;
+    privkey = test_mk_file_path(certsdir, "echserver.key");
+    if (privkey == NULL)
+        goto err;
+    rootcert = test_mk_file_path(certsdir, "rootcert.pem");
+    if (rootcert == NULL)
+        goto err;
+    ADD_ALL_TESTS(ech_ingest_test, OSSL_NELEM(ingest_tvs));
+    ADD_TEST(ech_store_null_calls);
+    ADD_ALL_TESTS(ech_test_file_read, OSSL_NELEM(fnames));
+    ADD_TEST(ech_api_basic_calls);
+    ADD_TEST(ech_boring_compat);
+    suite_combos = OSSL_NELEM(kem_str_list) * OSSL_NELEM(kdf_str_list)
+        * OSSL_NELEM(aead_str_list);
+    ADD_ALL_TESTS(test_ech_suites, suite_combos);
+    ADD_ALL_TESTS(test_ech_hrr, suite_combos);
+    ADD_ALL_TESTS(test_ech_early, suite_combos);
+    ADD_ALL_TESTS(ech_custom_test, suite_combos);
+    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 */
+    return 1;
+err:
+    return 0;
+#endif
+    return 1;
+}
+
+void cleanup_tests(void)
+{
+#ifndef OPENSSL_NO_ECH
+    OPENSSL_free(cert);
+    OPENSSL_free(privkey);
+    OPENSSL_free(rootcert);
+#endif
+}
diff --git a/test/ext_internal_test.c b/test/ext_internal_test.c
index 20cf708de27a3..c7d07d930ee1b 100644
--- a/test/ext_internal_test.c
+++ b/test/ext_internal_test.c
@@ -72,6 +72,13 @@ static EXT_LIST ext_list[] = {
     EXT_ENTRY(compress_certificate),
     EXT_ENTRY(early_data),
     EXT_ENTRY(certificate_authorities),
+#ifndef OPENSSL_NO_ECH
+    EXT_ENTRY(ech),
+    EXT_ENTRY(outer_extensions),
+#else
+    EXT_EXCEPTION(ech),
+    EXT_EXCEPTION(outer_extensions),
+#endif
     EXT_ENTRY(padding),
     EXT_ENTRY(psk),
     EXT_END(num_builtins)
diff --git a/test/packettest.c b/test/packettest.c
index 40b68d310a724..3eb0f73f044c7 100644
--- a/test/packettest.c
+++ b/test/packettest.c
@@ -570,9 +570,39 @@ static int test_PACKET_get_quic_length_prefixed(void)
 
     return 1;
 }
-
 #endif
 
+static int test_PACKET_msg_start(void)
+{
+    unsigned char buf[16] = { 0 };
+    PACKET pkt, subpkt;
+
+    if (!TEST_true(PACKET_buf_init(&pkt, buf, sizeof(buf))))
+        return 0;
+
+    if (!TEST_ptr_eq(PACKET_msg_start(&pkt), buf))
+        return 0;
+
+    if (!TEST_true(PACKET_forward(&pkt, 1))
+            || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf))
+        return 0;
+
+    if (!TEST_true(PACKET_get_sub_packet(&pkt, &subpkt, 1))
+            || !TEST_ptr_eq(PACKET_msg_start(&subpkt), buf)
+            || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf))
+        return 0;
+
+    if (!TEST_true(PACKET_forward(&subpkt, 1))
+            || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf))
+        return 0;
+
+    PACKET_null_init(&pkt);
+    if (!TEST_ptr_null(PACKET_msg_start(&pkt)))
+        return 0;
+
+    return 1;
+}
+
 int setup_tests(void)
 {
     unsigned int i;
@@ -607,5 +637,6 @@ int setup_tests(void)
     ADD_TEST(test_PACKET_get_quic_vlint);
     ADD_TEST(test_PACKET_get_quic_length_prefixed);
 #endif
+    ADD_TEST(test_PACKET_msg_start);
     return 1;
 }
diff --git a/test/recipes/20-test_app_ech.t b/test/recipes/20-test_app_ech.t
new file mode 100644
index 0000000000000..60a3b963094f8
--- /dev/null
+++ b/test/recipes/20-test_app_ech.t
@@ -0,0 +1,93 @@
+#! /usr/bin/env perl
+# Copyright 2020-2023 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 strict;
+use warnings;
+
+use OpenSSL::Test::Utils;
+use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_dir bldtop_file with/;
+
+setup("test_app_ech");
+
+plan skip_all => "ECH tests not supported in this build"
+    if disabled("ech") || disabled("tls1_3")
+       || disabled("ec") || disabled("ecx");
+
+plan tests => 13;
+
+ok(run(app(["openssl", "ech", "-help"])),
+   "Run openssl ech with help");
+ok(run(app(["openssl", "ech",
+                "-ech_version", "13",
+                "-public_name", "example.com",
+                "-out", "eg1.pem",
+                "-verbose",
+                "-text"])),
+   "Generate an ECH key pair for example.com");
+ok(run(app(["openssl", "ech",
+                "-suite", "0x10,2,2",
+                "-public_name", "example.com",
+                "-out", "eg2.pem",
+                "-text"])),
+   "Generate an ECDSA ECH key pair for example.com");
+ok(run(app(["openssl", "ech",
+                "-max_name_len", "13",
+                "-public_name", "example.com",
+                "-out", "eg2.pem",
+                "-text"])),
+   "Generate an ECH key pair for example.com with max name len 13");
+ok(run(app(["openssl", "ech",
+                "-in", "eg1.pem",
+                "-in", "eg2.pem",
+                "-out", "eg3.pem",
+                "-verbose"])),
+   "Catenate the ECH for example.com twice");
+ok(run(app(["openssl", "ech",
+                "-in", "eg3.pem",
+                "-select", "1",
+                "-verbose",
+                "-out", "eg4.pem"])),
+   "Select one ECH Config");
+
+with({ exit_checker => sub { return shift == 1; } },
+    sub { 
+		ok(run(app(["openssl", "ech" ])),
+		   "Run openssl ech with no arg");
+		ok(run(app(["openssl", "ech", "-nohelpatall"])),
+		   "Run openssl ech with unknown arg");
+		ok(run(app(["openssl", "ech", "nohelpatall"])),
+		   "Run openssl ech with unknown non arg");
+		ok(run(app(["openssl", "ech",
+		                "-ech_version", "0xfe09",
+		                "-public_name", "example.com",
+		                "-out", "eg1.pem",
+		                "-text"])),
+		   "Fail to generate an ECH key pair for old draft version");
+		ok(run(app(["openssl", "ech",
+		                "-suite", "not,a,good,one",
+		                "-public_name", "example.com",
+		                "-out", "eg2.pem",
+		                "-text"])),
+		   "Fail to generate an ECH key pair with bad suite");
+		ok(run(app(["openssl", "ech",
+		                "-max_name_len", "1300",
+		                "-public_name", "example.com",
+		                "-text"])),
+		   "(Fail to) Generate an ECH key pair for example.com with max name len 1300");
+		ok(run(app(["openssl", "ech",
+		                "-in", "eg1.pem",
+		                "-in", "eg2.pem",
+		                "-in", "eg3.pem",
+		                "-in", "eg4.pem",
+		                "-in", "eg1.pem",
+		                "-in", "eg2.pem",
+		                "-in", "eg3.pem",
+		                "-in", "eg4.pem"])),
+		   "Too many input files");
+});
diff --git a/test/recipes/30-test_ech.t b/test/recipes/30-test_ech.t
new file mode 100644
index 0000000000000..73ecd2a9c6a42
--- /dev/null
+++ b/test/recipes/30-test_ech.t
@@ -0,0 +1,21 @@
+#! /usr/bin/env perl
+# Copyright 2022 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");
+
+plan skip_all => "ECH tests not supported in this build"
+    if disabled("ech") || disabled("tls1_3") || disabled("ec") || disabled("ecx");
+
+plan tests => 1;
+
+ok(run(test(["ech_test", srctop_dir("test", "certs")])))
diff --git a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
index d36d58772b876..e7cf0b4e9e614 100644
--- a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
+++ b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
@@ -31,8 +31,6 @@ Header:
           ffdhe2048 (256)
           ffdhe3072 (257)
         extension_type=session_ticket(35), length=0
-        extension_type=application_layer_protocol_negotiation(16), length=11
-          ossltest
         extension_type=encrypt_then_mac(22), length=0
         extension_type=extended_master_secret(23), length=0
         extension_type=signature_algorithms(13), length=?
@@ -67,6 +65,8 @@ Header:
             key_exchange:  (len=32): ?
         extension_type=compress_certificate(27), length=3
           zlib (1)
+        extension_type=application_layer_protocol_negotiation(16), length=11
+          ossltest
 
 Sent Frame: Crypto
     Offset: 0
diff --git a/test/recipes/75-test_quicapi_data/ssltraceref.txt b/test/recipes/75-test_quicapi_data/ssltraceref.txt
index 7b7fa28c08871..b6daa53ae1e0c 100644
--- a/test/recipes/75-test_quicapi_data/ssltraceref.txt
+++ b/test/recipes/75-test_quicapi_data/ssltraceref.txt
@@ -31,8 +31,6 @@ Header:
           ffdhe2048 (256)
           ffdhe3072 (257)
         extension_type=session_ticket(35), length=0
-        extension_type=application_layer_protocol_negotiation(16), length=11
-          ossltest
         extension_type=encrypt_then_mac(22), length=0
         extension_type=extended_master_secret(23), length=0
         extension_type=signature_algorithms(13), length=?
@@ -65,6 +63,8 @@ Header:
             key_exchange:  (len=1216): ?
             NamedGroup: ecdh_x25519 (29)
             key_exchange:  (len=32): ?
+        extension_type=application_layer_protocol_negotiation(16), length=11
+          ossltest
 
 Sent Frame: Crypto
     Offset: 0
diff --git a/test/recipes/82-test_ech_client_server.t b/test/recipes/82-test_ech_client_server.t
new file mode 100644
index 0000000000000..9197c52f651b3
--- /dev/null
+++ b/test/recipes/82-test_ech_client_server.t
@@ -0,0 +1,342 @@
+#! /usr/bin/env perl
+# Copyright 2023-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 strict;
+use warnings;
+
+use IPC::Open3;
+use OpenSSL::Test qw/:DEFAULT srctop_file bldtop_file/;
+use OpenSSL::Test::Utils;
+use Symbol 'gensym';
+
+# servers randomly pick a port, then set this for clients to use
+# we also record the pid so we can kill it later if needed
+my $s_server_port = 0;
+my $s_server_pid = 0;
+my $s_client_match = 0;
+
+my $test_name = "test_ech_client_server";
+setup($test_name);
+
+plan skip_all => "$test_name requires EC cryptography"
+    if disabled("ec") || disabled("ecx");
+plan skip_all => "$test_name requires sock enabled"
+    if disabled("sock");
+plan skip_all => "$test_name requires TLSv1.3 enabled"
+    if disabled("tls1_3");
+plan skip_all => "$test_name is not available Windows or VMS"
+    if $^O =~ /^(VMS|MSWin32|msys)$/;
+
+plan tests => 18;
+
+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 $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");
+
+sub extract_ecl()
+{
+    # extract b64 encoded ECHConfigList from pem file
+    my $lb64 = "";
+    my $inwanted = 0;
+    open( my $fh, '<', $echconfig_pem ) or die "Can't open $echconfig_pem $!";
+    while( my $line = <$fh>) {
+        chomp $line;
+        if ( $line =~ /^-----BEGIN ECHCONFIG/) {
+            $inwanted = 1;
+        } elsif ( $line =~ /^-----END ECHCONFIG/) {
+            $inwanted = 0;
+        } elsif ($inwanted == 1) {
+            $lb64 .= $line;
+        }
+    }
+    print("base64 ECHConfigList: $lb64\n");
+    return($lb64);
+}
+
+my $good_b64 = extract_ecl();
+
+sub start_ech_client_server
+{
+    my ( $test_type, $winpattern ) = @_;
+
+    # start an s_server listening on some random port, with ECH enabled
+    # and willing to accept one request
+
+    # openssl s_server -accept 0 -naccept 1
+    #                  -key $server_key -cert $server_cert
+    #                  -key2 $server_key -cert2 $server_cert
+    #                  -ech_key $echconfig_pem
+    #                  -servername example.com
+    #                  -tls1_3
+    my @s_server_cmd;
+    if ($test_type eq "cid-free" ) {
+        # turn on trial-decrypt, so client can use random CID
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_trialdecrypt",
+                         "-tls1_3");
+    } else {
+        # default for all other tests (for now)
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_greaseretries",
+                         "-tls1_3");
+    }
+    print("@s_server_cmd\n");
+    $s_server_pid = open3(my $s_server_i, my $s_server_o,
+                             my $s_server_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_server_cmd);
+    # we're looking for...
+    # ACCEPT 0.0.0.0:45921
+    # ACCEPT [::]:45921
+    $s_server_port = "0";
+    while (<$s_server_o>) {
+        print($_);
+        chomp;
+        if (/^ACCEPT 0.0.0.0:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^ACCEPT \[::\]:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^Using default/) {
+            ;
+        } elsif (/^Added ECH key pair/) {
+            ;
+        } elsif (/^Loaded/) {
+            ;
+        } elsif (/^Setting secondary/) {
+            ;
+        } else {
+            last;
+        }
+    }
+    # openssl s_client -connect localhost:NNNNN
+    #                  -servername server.example
+    #                  -CAfile test/certs/rootcert.pem
+    #                  -ech_config_list "ADn+...AA="
+    #                  -prexit
+    my @s_client_cmd;
+    if ($test_type eq "GREASE-suite" ) {
+        # GREASE
+        @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 "lots-of-options" ) {
+        # real ECH with lots of options
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_outer_sni", "foodle.doodle",
+                         "-ech_select", "0",
+                         "-alpn", "http/1.1",
+                         "-ech_outer_alpn", "http451",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE-type" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease_type", "12345",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease",
+                         "-prexit");
+    } elsif ($test_type eq "no-outer" ) {
+        # Real ECH, no outer SNI
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_no_outer_sni",
+                         "-prexit");
+    } elsif ($test_type eq "bad-ech" ) {
+        # bad ECH
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", "AEH+DQA91wAgACCBdNrnZxqNrUXSyimqqnfmNG4lHtVsbmaaIeRoUoFWFQAEAAEAAQAOc2VydmVyLmV4YW1wbGUAAA==",
+                         "-prexit");
+    } elsif ($test_type eq "cid-free" ) {
+        # Real ECH, ignore CID
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } elsif ($test_type eq "cid-wrong" ) {
+        # Real ECH, ignore CID, no trial decrypt
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } else {
+        # Real ECH, and default
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-prexit");
+    }
+    print("@s_client_cmd\n");
+    local (*sc_input);
+    my $s_client_pid = open3(*sc_input, my $s_client_o,
+                             my $s_client_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_client_cmd);
+    print sc_input "Q\n";
+    close(sc_input);
+    waitpid($s_client_pid, 0);
+    # the output from s_client that we want to check is written to its
+    # stdout, e.g: "^ECH: success, yay!"
+    $s_client_match = 0;
+    while (<$s_client_o>) {
+        print($_);
+        chomp;
+        if (/$winpattern/) {
+            $s_client_match = 1;
+            last;
+        }
+    }
+    my $stillthere = kill 0, $s_server_pid;
+    if ($stillthere) {
+       print("s_server process ($s_server_pid) is not dead yet.\n");
+       kill 'HUP', $s_server_pid;
+    }
+}
+
+sub basic_test {
+    print("\n\nBasic test.\n");
+    my $tt = "basic";
+    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_client with ECH on command line");
+}
+
+sub wrong_test {
+    print("\n\nWrong ECHConfig test.\n");
+    # hardcoded 'cause we want a fail
+    my $tt="bad-ech",
+    my $win="^ECH: failed.retry-configs: -105";
+    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 ECH");
+}
+
+sub grease_test {
+    print("\n\nGREASE ECHConfig test.\n");
+    my $tt="GREASE";
+    my $win="^ECH: GREASE";
+    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 GREASE ECH");
+}
+
+sub grease_suite_test {
+    print("\n\nGREASE suite ECHConfig test.\n");
+    my $tt="GREASE-suite";
+    my $win="^ECH: GREASE";
+    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 GREASE-suite ECH");
+}
+
+sub grease_type_test {
+    print("\n\nGREASE type ECH test.\n");
+    my $tt="GREASE-type";
+    my $win="^ECH: GREASE";
+    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 GREASE-type ECH");
+}
+
+sub lots_of_options_test {
+    print("\n\nLots of options ECH test.\n");
+    my $tt="lots-of-options";
+    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_client with lots of ECH options");
+}
+
+sub no_outer_test {
+    print("\n\nNo outer SNI test.\n");
+    my $tt = "no-outer";
+    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_client with no outer SNI ECH");
+}
+
+sub cid_free_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-free";
+    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_client/s_server with no CID/trial decrypt");
+}
+
+sub cid_wrong_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-wrong";
+    my $win = "^ECH: failed";
+    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/s_server with no CID/no trial decrypt");
+}
+
+basic_test();
+wrong_test();
+grease_test();
+grease_suite_test();
+grease_type_test();
+lots_of_options_test();
+no_outer_test();
+cid_free_test();
+cid_wrong_test();
+
diff --git a/util/libssl.num b/util/libssl.num
index f64c8ac069316..83e25aeee65c6 100644
--- a/util/libssl.num
+++ b/util/libssl.num
@@ -606,3 +606,31 @@ SSL_CTX_get0_server_cert_type           ?	4_0_0	EXIST::FUNCTION:
 SSL_set_quic_tls_cbs                    ?	4_0_0	EXIST::FUNCTION:
 SSL_set_quic_tls_transport_params       ?	4_0_0	EXIST::FUNCTION:
 SSL_set_quic_tls_early_data_enabled     ?	4_0_0	EXIST::FUNCTION:
+OSSL_ECHSTORE_new                       ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_free                      ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_new_config                ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_write_pem                 ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_read_echconfiglist        ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_get1_info                 ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_downselect                ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_set1_key_and_read_pem     ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_read_pem                  ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_num_keys                  ?	4_0_0	EXIST::FUNCTION:ECH
+OSSL_ECHSTORE_flush_keys                ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_CTX_set1_echstore                   ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_set1_echstore                       ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_CTX_get1_echstore                   ?	4_0_0	EXIST::FUNCTION:ECH
+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
+SSL_ech_set1_outer_server_name          ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_ech_set1_outer_alpn_protos          ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_ech_set1_grease_suite               ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_ech_get1_retry_config               ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_CTX_ech_set1_outer_alpn_protos      ?	4_0_0	EXIST::FUNCTION:ECH
+SSL_set1_ech_config_list                ?	4_0_0	EXIST::FUNCTION:ECH
diff --git a/util/perl/TLSProxy/Message.pm b/util/perl/TLSProxy/Message.pm
index de923f0903f11..f082e7b16a9c7 100644
--- a/util/perl/TLSProxy/Message.pm
+++ b/util/perl/TLSProxy/Message.pm
@@ -99,6 +99,8 @@ use constant {
     EXT_RENEGOTIATE => 65281,
     EXT_NPN => 13172,
     EXT_CRYPTOPRO_BUG_EXTENSION => 0xfde8,
+    EXT_ECH => 0xfe0d,
+    EXT_ECH_OUTER => 0xfd00,
     EXT_UNKNOWN => 0xfffe,
     #Unknown extension that should appear last
     EXT_FORCE_LAST => 0xffff
diff --git a/util/platform_symbols/windows-symbols.txt b/util/platform_symbols/windows-symbols.txt
index 69fb23bfc1e7d..fa14f51834dec 100644
--- a/util/platform_symbols/windows-symbols.txt
+++ b/util/platform_symbols/windows-symbols.txt
@@ -89,6 +89,7 @@ __current_exception_context
 strlen
 strstr
 strchr
+strlen
 memmove
 strrchr
 memcmp