Commit 6c3edd4f3a for openssl.org
commit 6c3edd4f3a8ad16b0b17a3d2507752c78036a524
Author: sftcd <stephen.farrell@cs.tcd.ie>
Date: Mon May 5 14:23:55 2025 +0100
Add server-side handling of Encrypted Client Hello
Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/27561)
diff --git a/doc/man3/SSL_CTX_set_client_hello_cb.pod b/doc/man3/SSL_CTX_set_client_hello_cb.pod
index 41defadf30..78f5a89302 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/include/internal/ech_helpers.h b/include/internal/ech_helpers.h
index fcd607a57b..2fdf0a79ef 100644
--- a/include/internal/ech_helpers.h
+++ b/include/internal/ech_helpers.h
@@ -17,9 +17,42 @@
#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/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c
index 3151b5bfaf..78390f5cc3 100644
--- a/ssl/ech/ech_helper.c
+++ b/ssl/ech/ech_helper.c
@@ -13,8 +13,6 @@
#include "ech_local.h"
#include "internal/ech_helpers.h"
-/* TODO(ECH): move more code that's used by internals and test here */
-
/* 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";
@@ -52,3 +50,101 @@ int ossl_ech_make_enc_info(const unsigned char *encoding,
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
index 2f43877a83..fece841784 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -62,19 +62,11 @@ static void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg)
ossl_ech_pbuf(msg, hdata, hdatalen);
if (s->s3.handshake_dgst != NULL) {
if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) {
- OSSL_TRACE_BEGIN(TLS)
- {
- BIO_printf(trc_out, "ssl_handshake_hash failed\n");
- }
- OSSL_TRACE_END(TLS);
+ OSSL_TRACE(TLS, "ssl_handshake_hash failed\n");
ossl_ech_pbuf(msg, ddata, ddatalen);
}
}
- OSSL_TRACE_BEGIN(TLS)
- {
- BIO_printf(trc_out, "new transbuf:\n");
- }
- OSSL_TRACE_END(TLS);
+ OSSL_TRACE(TLS, "new transbuf:\n");
ossl_ech_pbuf(msg, s->ext.ech.transbuf, s->ext.ech.transbuf_len);
return;
}
@@ -1114,6 +1106,1033 @@ end:
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;
+#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,
+ NULL, NULL);
+ 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)
{
diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h
index 426d42e360..08730cb94e 100644
--- a/ssl/ech/ech_local.h
+++ b/ssl/ech/ech_local.h
@@ -118,6 +118,33 @@ typedef struct ossl_echstore_entry_st {
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 {
@@ -205,6 +232,22 @@ typedef struct ossl_ech_conn_st {
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
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index 02512da95d..502cf3197d 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -450,13 +450,8 @@ static const EXTENSION_DEFINITION ext_defs[] = {
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,
- /*
- * TODO(ECH): add server calls as per below in a bit
- * tls_parse_ctos_ech, tls_parse_stoc_ech,
- * tls_construct_stoc_ech, tls_construct_ctos_ech,
- */
- NULL, tls_parse_stoc_ech,
- NULL, tls_construct_ctos_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,
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 3b03a71948..7f455a3f7b 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -16,11 +16,6 @@
#ifndef OPENSSL_NO_ECH
#include <openssl/rand.h>
#include "internal/ech_helpers.h"
-/*
- * 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)
#endif
/* Used in the negotiate_dhe function */
@@ -2580,7 +2575,6 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
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));
diff --git a/ssl/statem/extensions_cust.c b/ssl/statem/extensions_cust.c
index 2808fab950..69138c1039 100644
--- a/ssl/statem/extensions_cust.c
+++ b/ssl/statem/extensions_cust.c
@@ -244,7 +244,8 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x,
SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION);
return 0;
}
- if (ossl_ech_copy_inner2outer(s, meth->ext_type, tind, pkt)
+ 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);
@@ -500,19 +501,7 @@ int ossl_tls_add_custom_ext_intern(SSL_CTX *ctx, custom_ext_methods *exts,
* for extension types that previously were not supported, but now are.
*/
if (SSL_extension_supported(ext_type)
-#if !defined(OPENSSL_NO_ECH) && defined(OPENSSL_ECH_ALLOW_CUST_INJECT)
- /*
- * Do this conditionally so we can test an ECH in TLSv1.2
- * via the custom extensions API.
- * OPENSSL_ECH_ALLOW_CUST_INJECT is defined (or not) in
- * include/openssl/ech.h and if defined enables a test in
- * test/ech_test.c
- */
- && ext_type != TLSEXT_TYPE_ech
&& ext_type != TLSEXT_TYPE_signed_certificate_timestamp)
-#else
- && ext_type != TLSEXT_TYPE_signed_certificate_timestamp)
-#endif
return 0;
/* Extension type must fit in 16 bits */
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index 7285c2d5cb..3535d06483 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -13,6 +13,11 @@
#include "internal/cryptlib.h"
#include "internal/ssl_unwrap.h"
+#ifndef OPENSSL_NO_ECH
+#include <openssl/rand.h>
+#include <openssl/trace.h>
+#endif
+
#define COOKIE_STATE_FORMAT_VERSION 1
/*
@@ -2439,3 +2444,163 @@ 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;
+
+ 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(s->ssl.ctx->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_local.h b/ssl/statem/statem_local.h
index b3c3eb18e2..cc9260c1f6 100644
--- a/ssl/statem/statem_local.h
+++ b/ssl/statem/statem_local.h
@@ -574,7 +574,9 @@ int tls_parse_stoc_server_cert_type(SSL_CONNECTION *s, PACKET *pkt,
EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
unsigned int context, X509 *x,
size_t chainidx);
-EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
+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,
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index 63b1278575..2f458cdf88 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -36,6 +36,10 @@
#define TICKET_NONCE_SIZE 8
+#ifndef OPENSSL_NO_ECH
+#include "../ech/ech_local.h"
+#endif
+
typedef struct {
ASN1_TYPE *kxBlob;
ASN1_TYPE *opaqueBlob;
@@ -1640,6 +1644,84 @@ MSG_PROCESS_RETURN tls_process_client_hello(SSL_CONNECTION *s, PACKET *pkt)
PACKET session_id, compression, extensions, cookie;
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))) {
@@ -1751,6 +1833,12 @@ err:
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;
}
@@ -2025,12 +2113,25 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s)
goto err;
}
- if (!s->hit
+ /*
+ * 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
@@ -2187,7 +2288,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;
@@ -2550,16 +2652,150 @@ 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)) {
/* 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/test/ech_test.c b/test/ech_test.c
index 17c8357351..998afeb03b 100644
--- a/test/ech_test.c
+++ b/test/ech_test.c
@@ -895,11 +895,15 @@ static int ech_ingest_test(int run)
* 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))) {
+ || ((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;
@@ -1143,6 +1147,7 @@ end:
#define OSSL_ECH_TEST_EARLY 2
#define OSSL_ECH_TEST_CUSTOM 3
#define OSSL_ECH_TEST_ENOE 4 /* early + no-ech */
+/* note: early-data is prohibited after HRR so no tests for that */
/*
* @brief ECH roundtrip test helper
@@ -1157,13 +1162,6 @@ end:
*
* The combo input is one of the #define'd OSSL_ECH_TEST_*
* values above.
- *
- * TODO(ECH): we're not yet really attempting ECH, but we currently
- * set the inputs as if we were doing ECH, yet don't expect to see
- * real ECH status outcomes, so while we do make calls to get that
- * status outcome, we don't compare vs. real expected results.
- * That's done via the "if (0 &&" clauses below which will be
- * removed once ECH is really being attempted.
*/
static int test_ech_roundtrip_helper(int idx, int combo)
{
@@ -1207,9 +1205,8 @@ static int test_ech_roundtrip_helper(int idx, int combo)
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)))
- goto end;
- if (!TEST_true(SSL_CTX_set_recv_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;
}
@@ -1241,58 +1238,38 @@ static int test_ech_roundtrip_helper(int idx, int combo)
goto end;
if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
goto end;
-#undef DROPFORNOW
-#ifdef DROPFORNOW
- /* TODO(ECH): we'll re-instate this once server-side ECH code is in */
if (!TEST_true(create_ssl_connection(serverssl, clientssl,
SSL_ERROR_NONE)))
goto end;
-#else
- /*
- * For this PR, check connections fail when client does ECH
- * and server doesn't, but work if client doesn't do ECH.
- * Added in early data for the no-ECH case because an
- * intermediate state of the code had an issue.
- */
- if (combo != OSSL_ECH_TEST_ENOE
- && !TEST_false(create_ssl_connection(serverssl, clientssl,
- SSL_ERROR_NONE)))
- goto end;
- if (combo == OSSL_ECH_TEST_ENOE
- && !TEST_true(create_ssl_connection(serverssl, clientssl,
- SSL_ERROR_NONE)))
- goto end;
-#endif
- serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
- if (verbose)
- TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
- if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
- goto end;
/* override cert verification */
SSL_set_verify_result(clientssl, X509_V_OK);
clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
if (verbose)
TEST_info("client status %d, %s, %s", clientstatus, cinner, couter);
- if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+ 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 */
- if (combo == OSSL_ECH_TEST_BASIC
- || combo == OSSL_ECH_TEST_HRR
+ if (combo == OSSL_ECH_TEST_BASIC || combo == OSSL_ECH_TEST_HRR
|| combo == OSSL_ECH_TEST_CUSTOM) {
res = 1;
goto end;
}
/* continue for EARLY test */
-#ifdef DROPFORNOW
- /* TODO(ECH): turn back on later */
if (combo != OSSL_ECH_TEST_EARLY && combo != OSSL_ECH_TEST_ENOE)
goto end;
-#else
- if (combo != OSSL_ECH_TEST_ENOE) {
- res = 1;
- goto end;
- }
-#endif
/* shutdown for start over */
sess = SSL_get1_session(clientssl);
OPENSSL_free(sinner);
@@ -1312,8 +1289,8 @@ static int test_ech_roundtrip_helper(int idx, int combo)
|| !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),
+ || !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;
@@ -1326,17 +1303,25 @@ static int test_ech_roundtrip_helper(int idx, int combo)
|| !TEST_true(SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes))
|| !TEST_mem_eq(buf, readbytes, ed, sizeof(ed)))
goto end;
- serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter);
- if (verbose)
- TEST_info("server status %d, %s, %s", serverstatus, sinner, souter);
- if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS))
- goto end;
/* override cert verification */
SSL_set_verify_result(clientssl, X509_V_OK);
clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
if (verbose)
TEST_info("client status %d, %s, %s", clientstatus, cinner, couter);
- if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS))
+ 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;
diff --git a/util/platform_symbols/windows-symbols.txt b/util/platform_symbols/windows-symbols.txt
index ef82cb14c8..ccf3af2b3c 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