From 497daff2b90fd7b99399fa49c75cf8d490656760 Mon Sep 17 00:00:00 2001 From: sftcd Date: Mon, 5 May 2025 14:23:55 +0100 Subject: [PATCH] Add server-side handling of Encrypted Client Hello Reviewed-by: Matt Caswell Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/27561) --- doc/man3/SSL_CTX_set_client_hello_cb.pod | 2 + include/internal/ech_helpers.h | 33 + ssl/ech/ech_helper.c | 100 +- ssl/ech/ech_internal.c | 1025 ++++++++++++++++++++- ssl/ech/ech_local.h | 43 + ssl/ech/ech_ssl_apis.c | 2 +- ssl/statem/extensions.c | 9 +- ssl/statem/extensions_clnt.c | 8 +- ssl/statem/extensions_cust.c | 26 +- ssl/statem/extensions_srvr.c | 156 +++- ssl/statem/statem_clnt.c | 2 +- ssl/statem/statem_local.h | 4 +- ssl/statem/statem_srvr.c | 252 ++++- test/ech_test.c | 95 +- util/platform_symbols/windows-symbols.txt | 1 + 15 files changed, 1647 insertions(+), 111 deletions(-) diff --git a/doc/man3/SSL_CTX_set_client_hello_cb.pod b/doc/man3/SSL_CTX_set_client_hello_cb.pod index 74468ab8ac1..2cf5c4e5735 100644 --- a/doc/man3/SSL_CTX_set_client_hello_cb.pod +++ b/doc/man3/SSL_CTX_set_client_hello_cb.pod @@ -104,6 +104,8 @@ resumption and the historical servername callback. The SSL_client_hello_* family of functions may only be called from code executing within a ClientHello callback. +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 3e13936e2e9..946ad2f1dcf 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 4303a1b6eb0..b2c2a87aa4f 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 017872d60cc..7fdcf3c5c75 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -60,15 +60,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; } @@ -1018,7 +1014,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, 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; + + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; unsigned int hashlen = 0; unsigned char hashval[EVP_MAX_MD_SIZE]; @@ -1100,6 +1096,1019 @@ 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 inital 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) { @@ -1137,7 +2146,7 @@ int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, } else { /* just add new octets */ if ((t1 = OPENSSL_realloc(s->ext.ech.transbuf, - s->ext.ech.transbuf_len + blen)) == NULL) + 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); diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 6ae0b426489..7dcd553de98 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/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c index 601a8ad1621..436e0fa71cc 100644 --- a/ssl/ech/ech_ssl_apis.c +++ b/ssl/ech/ech_ssl_apis.c @@ -185,7 +185,7 @@ int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni) return SSL_ECH_STATUS_GREASE_ECH; return SSL_ECH_STATUS_GREASE; } - if ((s->options & SSL_OP_ECH_GREASE) !=0 && s->ext.ech.attempted != 1) + 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 diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 78e3c312722..bb225decd37 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -498,13 +498,8 @@ static const EXTENSION_DEFINITION ext_defs[] = { 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 }, { diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index ccb29d71386..461463efa2a 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -14,12 +14,7 @@ #include "statem_local.h" #ifndef OPENSSL_NO_ECH # include -#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) +# include "internal/ech_helpers.h" #endif EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, @@ -2524,7 +2519,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 bda7e460b9d..5ea06eb0a72 100644 --- a/ssl/statem/extensions_cust.c +++ b/ssl/statem/extensions_cust.c @@ -209,8 +209,8 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, 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); + "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; @@ -219,9 +219,9 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, 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); + "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) { @@ -247,8 +247,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) - != OSSL_ECH_SAME_EXT_DONE) { + 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; @@ -505,19 +505,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 884b9ca4891..91a076a3840 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 @@ -2420,3 +2425,152 @@ 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 exptected.\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_clnt.c b/ssl/statem/statem_clnt.c index 4514b630668..ff11d98080e 100644 --- a/ssl/statem/statem_clnt.c +++ b/ssl/statem/statem_clnt.c @@ -1312,7 +1312,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, goto err; } OPENSSL_free(s->ext.ech.innerch); - s->ext.ech.innerch = (unsigned char*)inner_mem->data; + s->ext.ech.innerch = (unsigned char *)inner_mem->data; inner_mem->data = NULL; s->ext.ech.innerch_len = innerlen; /* add inner to transcript */ diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h index 6328e1ee828..8ba5ef242a8 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 80d09d76e1c..ac0d7d761e1 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/test/ech_test.c b/test/ech_test.c index 968fca896d1..2f3cddc27b1 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -893,11 +893,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; @@ -1141,6 +1145,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 @@ -1155,13 +1160,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) { @@ -1205,10 +1203,9 @@ 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))) + 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) { @@ -1239,58 +1236,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); @@ -1310,8 +1287,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; @@ -1324,17 +1301,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 69fb23bfc1e..fa14f51834d 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 -- 2.47.2