blob: 338c111bfd07c05217f74ca0e648b9fbf32cefdb [file] [log] [blame] [edit]
/*
* Copyright 2024-2026 The OpenSSL Project Authors. All Rights Reserved.
*
* Licensed under the OpenSSL license (the "License"). You may not use
* this file except in compliance with the License. You can obtain a copy
* in the file LICENSE in the source distribution or at
* https://www.openssl.org/source/license.html
*/
#include <openssl/ssl.h>
#include <openssl/ech.h>
#include <openssl/rand.h>
#include <openssl/kdf.h>
#include "internal/ech_helpers.h"
#include "internal/ssl_unwrap.h"
#include "../ssl_local.h"
#include "../statem/statem_local.h"
#include "ech_local.h"
#ifndef OPENSSL_NO_ECH
/*
* Strings used in ECH crypto derivations (odd format for EBCDIC goodness)
*/
/* "ech accept confirmation" */
static const char OSSL_ECH_ACCEPT_CONFIRM_STRING[] = "\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e";
/* "hrr ech accept confirmation" */
static const char OSSL_ECH_HRR_CONFIRM_STRING[] = "\x68\x72\x72\x20\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e";
/* ECH internal API functions */
#ifdef OSSL_ECH_SUPERVERBOSE
/* ascii-hex print a buffer nicely for debug/interop purposes */
void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen)
{
OSSL_TRACE_BEGIN(TLS)
{
if (msg == NULL) {
BIO_printf(trc_out, "msg is NULL\n");
} else if (buf == NULL || blen == 0) {
BIO_printf(trc_out, "%s: buf is %p\n", msg, (void *)buf);
BIO_printf(trc_out, "%s: blen is %lu\n", msg, (unsigned long)blen);
} else {
BIO_printf(trc_out, "%s (%lu)\n", msg, (unsigned long)blen);
BIO_dump_indent(trc_out, buf, (int)blen, 4);
}
}
OSSL_TRACE_END(TLS);
return;
}
/* trace out transcript */
static void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg)
{
size_t hdatalen = 0;
unsigned char *hdata = NULL;
unsigned char ddata[EVP_MAX_MD_SIZE];
size_t ddatalen;
if (s == NULL)
return;
hdatalen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata);
ossl_ech_pbuf(msg, hdata, hdatalen);
if (s->s3.handshake_dgst != NULL) {
if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) {
OSSL_TRACE(TLS, "ssl_handshake_hash failed\n");
ossl_ech_pbuf(msg, ddata, ddatalen);
}
}
OSSL_TRACE(TLS, "new transbuf:\n");
ossl_ech_pbuf(msg, s->ext.ech.transbuf, s->ext.ech.transbuf_len);
return;
}
#endif
static OSSL_ECHSTORE_ENTRY *ossl_echstore_entry_dup(const OSSL_ECHSTORE_ENTRY *orig)
{
OSSL_ECHSTORE_ENTRY *ret = NULL;
if (orig == NULL)
return NULL;
ret = OPENSSL_zalloc(sizeof(*ret));
if (ret == NULL)
return NULL;
ret->version = orig->version;
if (orig->public_name != NULL) {
ret->public_name = OPENSSL_strdup(orig->public_name);
if (ret->public_name == NULL)
goto err;
}
ret->pub_len = orig->pub_len;
if (orig->pub != NULL) {
ret->pub = OPENSSL_memdup(orig->pub, orig->pub_len);
if (ret->pub == NULL)
goto err;
}
ret->nsuites = orig->nsuites;
ret->suites = OPENSSL_memdup(orig->suites, sizeof(OSSL_HPKE_SUITE) * ret->nsuites);
if (ret->suites == NULL)
goto err;
ret->max_name_length = orig->max_name_length;
ret->config_id = orig->config_id;
if (orig->exts != NULL) {
ret->exts = sk_OSSL_ECHEXT_deep_copy(orig->exts, ossl_echext_dup,
ossl_echext_free);
if (ret->exts == NULL)
goto err;
}
ret->loadtime = orig->loadtime;
if (orig->keyshare != NULL) {
if (!EVP_PKEY_up_ref(orig->keyshare))
goto err;
ret->keyshare = orig->keyshare;
}
ret->for_retry = orig->for_retry;
if (orig->encoded != NULL) {
ret->encoded_len = orig->encoded_len;
ret->encoded = OPENSSL_memdup(orig->encoded, ret->encoded_len);
if (ret->encoded == NULL)
goto err;
}
return ret;
err:
ossl_echstore_entry_free(ret);
return NULL;
}
/* duplicate an OSSL_ECHSTORE as needed */
OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old)
{
OSSL_ECHSTORE *cp = NULL;
if (old == NULL)
return NULL;
cp = OPENSSL_zalloc(sizeof(*cp));
if (cp == NULL)
return NULL;
cp->libctx = old->libctx;
if (old->propq != NULL) {
cp->propq = OPENSSL_strdup(old->propq);
if (cp->propq == NULL)
goto err;
}
if (old->entries != NULL) {
cp->entries = sk_OSSL_ECHSTORE_ENTRY_deep_copy(old->entries,
ossl_echstore_entry_dup,
ossl_echstore_entry_free);
if (cp->entries == NULL)
goto err;
}
return cp;
err:
OSSL_ECHSTORE_free(cp);
return NULL;
}
void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce)
{
if (ce == NULL)
return;
OSSL_ECHSTORE_free(ce->es);
OPENSSL_free(ce->alpn_outer);
return;
}
static void ech_free_stashed_key_shares(OSSL_ECH_CONN *ec)
{
size_t i;
if (ec == NULL)
return;
for (i = 0; i != ec->num_ks_pkey; i++) {
EVP_PKEY_free(ec->ks_pkey[i]);
ec->ks_pkey[i] = NULL;
}
ec->num_ks_pkey = 0;
return;
}
void ossl_ech_conn_clear(OSSL_ECH_CONN *ec)
{
if (ec == NULL)
return;
OSSL_ECHSTORE_free(ec->es);
OPENSSL_free(ec->outer_hostname);
OPENSSL_free(ec->alpn_outer);
OPENSSL_free(ec->former_inner);
OPENSSL_free(ec->transbuf);
OPENSSL_free(ec->innerch);
OPENSSL_free(ec->grease_suite);
OPENSSL_free(ec->sent);
OPENSSL_free(ec->returned);
OPENSSL_free(ec->pub);
OSSL_HPKE_CTX_free(ec->hpke_ctx);
OPENSSL_free(ec->encoded_inner);
ech_free_stashed_key_shares(ec);
return;
}
/* called from ssl/ssl_lib.c: ossl_ssl_connection_new_int */
int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx,
const SSL_METHOD *method)
{
memset(&s->ext.ech, 0, sizeof(s->ext.ech));
if (ctx->ext.ech.es != NULL
&& (s->ext.ech.es = ossl_echstore_dup(ctx->ext.ech.es)) == NULL)
goto err;
s->ext.ech.cb = ctx->ext.ech.cb;
if (ctx->ext.ech.alpn_outer != NULL) {
s->ext.ech.alpn_outer = OPENSSL_memdup(ctx->ext.ech.alpn_outer,
ctx->ext.ech.alpn_outer_len);
if (s->ext.ech.alpn_outer == NULL)
goto err;
s->ext.ech.alpn_outer_len = ctx->ext.ech.alpn_outer_len;
}
/* initialise type/cid to unknown */
s->ext.ech.attempted_type = OSSL_ECH_type_unknown;
s->ext.ech.attempted_cid = OSSL_ECH_config_id_unset;
if (s->ext.ech.es != NULL)
s->ext.ech.attempted = 1;
if ((ctx->options & SSL_OP_ECH_GREASE) != 0)
s->options |= SSL_OP_ECH_GREASE;
return 1;
err:
OSSL_ECHSTORE_free(s->ext.ech.es);
s->ext.ech.es = NULL;
OPENSSL_free(s->ext.ech.alpn_outer);
s->ext.ech.alpn_outer = NULL;
s->ext.ech.alpn_outer_len = 0;
return 0;
}
/*
* Assemble the set of ECHConfig values to return as retry-configs.
* The caller (stoc ECH extension handler) needs to OPENSSL_free the rcfgs
* The rcfgs itself is missing the outer length to make it an ECHConfigList
* so the caller adds that using WPACKET functions
*/
int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs,
size_t *rcfgslen)
{
OSSL_ECHSTORE *es = NULL;
OSSL_ECHSTORE_ENTRY *ee = NULL;
int i, num = 0;
size_t retslen = 0;
unsigned char *tmp = NULL, *rets = NULL;
if (s == NULL || rcfgs == NULL || rcfgslen == NULL)
return 0;
es = s->ext.ech.es;
if (es != NULL && es->entries != NULL)
num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
for (i = 0; i != num; i++) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
if (ee != NULL && ee->for_retry == OSSL_ECH_FOR_RETRY) {
if (ee->encoded_len > SIZE_MAX - retslen)
goto err;
tmp = (unsigned char *)OPENSSL_realloc(rets,
retslen + ee->encoded_len);
if (tmp == NULL)
goto err;
rets = tmp;
memcpy(rets + retslen, ee->encoded, ee->encoded_len);
retslen += ee->encoded_len;
}
}
*rcfgs = rets;
*rcfgslen = retslen;
return 1;
err:
OPENSSL_free(rets);
*rcfgs = NULL;
*rcfgslen = 0;
return 0;
}
/* GREASEy constants */
#define OSSL_ECH_MAX_GREASE_PUB 0x100 /* buffer size for 'enc' values */
#define OSSL_ECH_MAX_GREASE_CT 0x200 /* max GREASEy ciphertext we'll emit */
/*
* Send a random value that looks like a real ECH.
*
* We do GREASEing as follows:
* - always HKDF-SHA256
* - always AES-128-GCM
* - random config ID, even for requests to same server in same session
* - random enc
* - random looking payload, randomly 144, 176, 208, 240 bytes, no correlation with server
*/
int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt)
{
OSSL_HPKE_SUITE hpke_suite_in = OSSL_HPKE_SUITE_DEFAULT;
OSSL_HPKE_SUITE *hpke_suite_in_p = NULL;
OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
size_t pp_at_start = 0, pp_at_end = 0;
size_t senderpub_len = OSSL_ECH_MAX_GREASE_PUB;
size_t cipher_len = 0, cipher_len_jitter = 0;
unsigned char cid, senderpub[OSSL_ECH_MAX_GREASE_PUB];
unsigned char cipher[OSSL_ECH_MAX_GREASE_CT];
SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
if (!WPACKET_get_total_written(pkt, &pp_at_start)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
/* randomly select cipher_len to be one of 144, 176, 208, 244 */
if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
cipher_len_jitter = cid % 4;
cipher_len = 144;
cipher_len += 32 * cipher_len_jitter;
/* generate a random (1 octet) client id */
if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
s->ext.ech.attempted_cid = cid;
hpke_suite_in_p = &hpke_suite;
if (s->ext.ech.grease_suite != NULL) {
if (OSSL_HPKE_str2suite(s->ext.ech.grease_suite, &hpke_suite_in) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
hpke_suite_in_p = &hpke_suite_in;
}
if (OSSL_HPKE_get_grease_value(hpke_suite_in_p, &hpke_suite,
senderpub, &senderpub_len,
cipher, cipher_len,
sctx->libctx, sctx->propq)
!= 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type)
|| !WPACKET_start_sub_packet_u16(pkt)
|| !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE)
|| !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id)
|| !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id)
|| !WPACKET_put_bytes_u8(pkt, cid)
|| !WPACKET_sub_memcpy_u16(pkt, senderpub, senderpub_len)
|| !WPACKET_sub_memcpy_u16(pkt, cipher, cipher_len)
|| !WPACKET_close(pkt)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
/* record the ECH sent so we can re-tx same if we hit an HRR */
OPENSSL_free(s->ext.ech.sent);
if (!WPACKET_get_total_written(pkt, &pp_at_end)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
s->ext.ech.sent_len = pp_at_end - pp_at_start;
s->ext.ech.sent = OPENSSL_malloc(s->ext.ech.sent_len);
if (s->ext.ech.sent == NULL) {
s->ext.ech.sent_len = 0;
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
memcpy(s->ext.ech.sent, WPACKET_get_curr(pkt) - s->ext.ech.sent_len,
s->ext.ech.sent_len);
s->ext.ech.grease = OSSL_ECH_IS_GREASE;
OSSL_TRACE_BEGIN(TLS)
{
BIO_printf(trc_out, "ECH - sending GREASE\n");
}
OSSL_TRACE_END(TLS);
return 1;
}
/*
* Search the ECH store for one that's a match. If no outer_name was set via
* API then we just take the 1st match where we locally support the HPKE suite.
* If OTOH, an outer_name was provided via API then we prefer the first that
* matches that. Name comparison is via case-insensitive exact matches.
*/
int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee,
OSSL_HPKE_SUITE *suite)
{
int namematch = 0, nameoverride = 0, suitematch = 0, num, cind = 0;
unsigned int csuite = 0, tsuite = 0;
size_t hnlen = 0;
OSSL_ECHSTORE_ENTRY *lee = NULL, *tee = NULL;
OSSL_ECHSTORE *es = NULL;
char *hn = NULL;
if (s == NULL || s->ext.ech.es == NULL || ee == NULL || suite == NULL)
return 0;
*ee = NULL;
es = s->ext.ech.es;
if (es->entries == NULL)
return 0;
num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
/* allow API-set pref to override */
hn = s->ext.ech.outer_hostname;
hnlen = (hn == NULL ? 0 : (unsigned int)strlen(hn));
if (hnlen != 0)
nameoverride = 1;
if (s->ext.ech.no_outer == 1) {
hn = NULL;
hnlen = 0;
nameoverride = 1;
}
for (cind = 0; cind < num && (suitematch == 0 || namematch == 0); cind++) {
lee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cind);
if (lee == NULL || lee->version != OSSL_ECH_RFC9849_VERSION)
continue;
if (nameoverride == 1 && hnlen == 0) {
namematch = 1;
} else {
namematch = 0;
if (hnlen == 0
|| (lee->public_name != NULL
&& strlen(lee->public_name) == hnlen
&& OPENSSL_strncasecmp(hn, (char *)lee->public_name,
hnlen)
== 0))
namematch = 1;
}
suitematch = 0;
for (csuite = 0; csuite != lee->nsuites && suitematch == 0; csuite++) {
if (OSSL_HPKE_suite_check(lee->suites[csuite]) == 1) {
if (tee == NULL) { /* remember 1st suite match for override */
tee = lee;
tsuite = csuite;
}
suitematch = 1;
if (namematch == 1) { /* pick this one if both "fit" */
*suite = lee->suites[csuite];
*ee = lee;
break;
}
}
}
}
if (tee != NULL && nameoverride == 1
&& (namematch == 0 || suitematch == 0)) {
*suite = tee->suites[tsuite];
*ee = tee;
} else if (namematch == 0 || suitematch == 0) {
/* no joy */
return 0;
}
if (*ee == NULL || (*ee)->pub_len == 0 || (*ee)->pub == NULL)
return 0;
return 1;
}
/* Make up the ClientHelloInner and EncodedClientHelloInner buffers */
int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded,
size_t *encoded_len)
{
int rv = 0;
size_t nraws = 0, ind = 0, innerlen = 0;
WPACKET inner = { 0 }; /* "fake" pkt for inner */
BUF_MEM *inner_mem = NULL;
RAW_EXTENSION *raws = NULL;
/* basic checks */
if (s == NULL)
return 0;
if (s->ext.ech.es == NULL || s->clienthello == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
if ((inner_mem = BUF_MEM_new()) == NULL
|| !WPACKET_init(&inner, inner_mem)
/* We don't add the type and 3-octet header as usually done */
/* Add ver/rnd/sess-id/suites to buffer */
|| !WPACKET_put_bytes_u16(&inner, s->client_version)
|| !WPACKET_memcpy(&inner, s->ext.ech.client_random, SSL3_RANDOM_SIZE)
/* Session ID is forced to zero in the encoded inner */
|| !WPACKET_sub_memcpy_u8(&inner, NULL, 0)
/* Ciphers supported */
|| !WPACKET_start_sub_packet_u16(&inner)
|| !ssl_cipher_list_to_bytes(s, SSL_get_ciphers(&s->ssl), &inner)
|| !WPACKET_close(&inner)
/* COMPRESSION */
|| !WPACKET_start_sub_packet_u8(&inner)
/* Add the NULL compression method */
|| !WPACKET_put_bytes_u8(&inner, 0)
|| !WPACKET_close(&inner)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* Now handle extensions */
if (!WPACKET_start_sub_packet_u16(&inner)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* Grab a pointer to the already constructed extensions */
raws = s->clienthello->pre_proc_exts;
nraws = s->clienthello->pre_proc_exts_len;
if (raws == NULL || nraws < TLSEXT_IDX_num_builtins) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* We put ECH-compressed stuff first (if any), because we can */
if (s->ext.ech.n_outer_only > 0) {
if (!WPACKET_put_bytes_u16(&inner, TLSEXT_TYPE_outer_extensions)
|| !WPACKET_start_sub_packet_u16(&inner)
/* redundant encoding of more-or-less the same thing */
|| !WPACKET_start_sub_packet_u8(&inner)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* add the types for each of the compressed extensions now */
for (ind = 0; ind != s->ext.ech.n_outer_only; ind++) {
if (!WPACKET_put_bytes_u16(&inner, s->ext.ech.outer_only[ind])) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
}
/* close the 2 sub-packets with the compressed types */
if (!WPACKET_close(&inner) || !WPACKET_close(&inner)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
}
/* now copy the rest, as "proper" exts, into encoded inner */
for (ind = 0; ind < TLSEXT_IDX_num_builtins; ind++) {
if (raws[ind].present == 0 || ossl_ech_2bcompressed((int)ind) == 1)
continue;
if (!WPACKET_put_bytes_u16(&inner, raws[ind].type)
|| !WPACKET_sub_memcpy_u16(&inner, PACKET_data(&raws[ind].data),
PACKET_remaining(&raws[ind].data))) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
}
if (!WPACKET_close(&inner) /* close the encoded inner packet */
|| !WPACKET_get_length(&inner, &innerlen)) { /* len for inner CH */
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
*encoded = (unsigned char *)inner_mem->data;
inner_mem->data = NULL; /* keep BUF_MEM_free happy */
*encoded_len = innerlen;
/* and clean up */
rv = 1;
err:
WPACKET_cleanup(&inner);
BUF_MEM_free(inner_mem);
return rv;
}
/*
* Find ECH acceptance signal in a SH
* hrr is 1 if this is for an HRR, otherwise for SH
* acbuf is (a preallocated) 8 octet buffer
* shbuf is a pointer to the SH buffer
* shlen is the length of the SH buf
* return: 1 for success, 0 otherwise
*/
int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr,
unsigned char acbuf[OSSL_ECH_SIGNAL_LEN])
{
unsigned char *acp = NULL;
if (hrr == 0) {
acp = s->s3.server_random + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN;
} else { /* was set in extension handler */
if (s->ext.ech.hrrsignal_p == NULL)
return 0;
acp = s->ext.ech.hrrsignal;
}
memcpy(acbuf, acp, OSSL_ECH_SIGNAL_LEN);
return 1;
}
/*
* reset the handshake buffer for transcript after ECH is good
* buf is the data to put into the transcript (inner CH if no HRR)
* blen is the length of buf
* return 1 for success
*/
int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf,
size_t blen)
{
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("RESET transcript to", buf, blen);
#endif
if (s->s3.handshake_buffer != NULL) {
if (BIO_reset(s->s3.handshake_buffer) < 0)
return 0;
} else {
s->s3.handshake_buffer = BIO_new(BIO_s_mem());
if (s->s3.handshake_buffer == NULL)
return 0;
(void)BIO_set_close(s->s3.handshake_buffer, BIO_CLOSE);
}
EVP_MD_CTX_free(s->s3.handshake_dgst);
s->s3.handshake_dgst = NULL;
/* providing nothing at all is a real use (mid-HRR) */
if (buf != NULL && blen > 0)
BIO_write(s->s3.handshake_buffer, (void *)buf, (int)blen);
return 1;
}
/*
* To control the number of zeros added after an EncodedClientHello - we pad
* to a target number of octets or, if there are naturally more, to a number
* divisible by the defined increment (we also do the spec-recommended SNI
* padding thing first)
*/
#define OSSL_ECH_PADDING_TARGET 128 /* ECH cleartext padded to at least this */
#define OSSL_ECH_PADDING_INCREMENT 32 /* ECH padded to a multiple of this */
/*
* figure out how much padding for cleartext (on client)
* ee is the chosen ECHConfig
* return overall length to use including padding or zero on error
*
* "Recommended" inner SNI padding scheme as per spec (section 6.1.3)
* Might remove the mnl stuff later - overall message padding seems
* better really, BUT... we might want to keep this if others (e.g.
* browsers) do it so as to not stand out compared to them.
*
* The "+ 9" constant below is from the specification and is the
* expansion comparing a string length to an encoded SNI extension.
* Same is true of the 31/32 formula below.
*
* Note that the AEAD tag will be added later, so if we e.g. have
* a padded cleartext of 128 octets, the ciphertext will be 144
* octets.
*/
size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
size_t encoded_len)
{
size_t length_of_padding = 0, length_with_snipadding = 0;
size_t innersnipadding = 0, length_with_padding = 0;
size_t mnl = 0, isnilen = 0;
if (s == NULL || ee == NULL)
return 0;
mnl = ee->max_name_length;
if (mnl != 0) {
/* do weirder padding if SNI present in inner */
if (s->ext.hostname != NULL) {
isnilen = strlen(s->ext.hostname) + 9;
innersnipadding = (mnl > isnilen) ? (int)(mnl - isnilen) : 0;
} else {
innersnipadding = (int)mnl + 9;
}
}
/* padding is after the inner client hello has been encoded */
length_with_snipadding = innersnipadding + (int)encoded_len;
length_of_padding = 31 - ((length_with_snipadding - 1) % 32);
length_with_padding = (int)encoded_len + length_of_padding
+ innersnipadding;
/*
* Finally - make sure final result is longer than padding target
* and a multiple of our padding increment.
* This is a local addition - we might want to take it out if it makes
* us stick out; or if we take out the above more (uselessly:-)
* complicated scheme above, we may only need this in the end.
*/
if ((length_with_padding % OSSL_ECH_PADDING_INCREMENT) != 0)
length_with_padding += OSSL_ECH_PADDING_INCREMENT
- (length_with_padding % OSSL_ECH_PADDING_INCREMENT);
while (length_with_padding < OSSL_ECH_PADDING_TARGET)
length_with_padding += OSSL_ECH_PADDING_INCREMENT;
OSSL_TRACE_BEGIN(TLS)
{
BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %zu "
"lop: %zu, clear_len (len with padding): %zu, orig: %zu\n",
mnl, length_with_snipadding, length_of_padding,
length_with_padding, encoded_len);
}
OSSL_TRACE_END(TLS);
return length_with_padding;
}
/*
* Calculate AAD and do ECH encryption
* pkt is the packet to send
* return 1 for success, other otherwise
*
* 1. Make up the AAD: the encoded outer, with ECH ciphertext octets zero'd
* 2. Do the encryption
* 3. Put the ECH back into the encoding
* 4. Encode the outer (again!)
*/
int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt)
{
int rv = 0;
size_t cipherlen = 0, aad_len = 0, mypub_len = 0, clear_len = 0;
size_t encoded_inner_len = 0;
unsigned char *clear = NULL, *aad = NULL, *mypub = NULL;
unsigned char *encoded_inner = NULL, *cipher_loc = NULL;
if (s == NULL)
return 0;
if (s->ext.ech.es == NULL || s->ext.ech.es->entries == NULL
|| pkt == NULL || s->ssl.ctx == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* values calculated in tls_construct_ctos_ech */
encoded_inner = s->ext.ech.encoded_inner;
encoded_inner_len = s->ext.ech.encoded_inner_len;
clear_len = s->ext.ech.clearlen;
cipherlen = s->ext.ech.cipherlen;
if (!WPACKET_get_total_written(pkt, &aad_len) || aad_len < 4) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
aad_len -= 4; /* ECH/HPKE aad starts after type + 3-octet len */
aad = WPACKET_get_curr(pkt) - aad_len;
/* where we'll replace zeros with ciphertext */
cipher_loc = aad + s->ext.ech.cipher_offset;
/*
* close the extensions of the CH - we skipped doing this
* earlier when encoding extensions, to allow for adding the
* ECH here (when doing ECH) - see tls_construct_extensions()
* towards the end
*/
if (!WPACKET_close(pkt)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("EAAE: aad", aad, aad_len);
#endif
clear = OPENSSL_zalloc(clear_len); /* zeros incl. padding */
if (clear == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
memcpy(clear, encoded_inner, encoded_inner_len);
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("EAAE: padded clear", clear, clear_len);
#endif
/* we're done with this now */
OPENSSL_free(s->ext.ech.encoded_inner);
s->ext.ech.encoded_inner = NULL;
rv = OSSL_HPKE_seal(s->ext.ech.hpke_ctx, cipher_loc,
&cipherlen, aad, aad_len, clear, clear_len);
OPENSSL_free(clear);
if (rv != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("EAAE: cipher", cipher_loc, cipherlen);
ossl_ech_pbuf("EAAE: hpke mypub", mypub, mypub_len);
/* re-use aad_len for tracing */
WPACKET_get_total_written(pkt, &aad_len);
ossl_ech_pbuf("EAAE pkt aftr", WPACKET_get_curr(pkt) - aad_len, aad_len);
#endif
return 1;
err:
return 0;
}
/*
* print info about the ECH-status of an SSL connection
* out is the BIO to use (e.g. stdout/whatever)
* selector OSSL_ECH_SELECT_ALL or just one of the SSL_ECH values
*/
void ossl_ech_status_print(BIO *out, SSL_CONNECTION *s, int selector)
{
int num = 0, i, has_priv, for_retry;
size_t j;
time_t secs = 0;
char *pn = NULL, *ec = NULL;
OSSL_ECHSTORE *es = NULL;
#ifdef OSSL_ECH_SUPERVERBOSE
BIO_printf(out, "ech_status_print\n");
BIO_printf(out, "s=%p\n", (void *)s);
#endif
BIO_printf(out, "ech_attempted=%d\n", s->ext.ech.attempted);
BIO_printf(out, "ech_attempted_type=0x%4x\n",
s->ext.ech.attempted_type);
if (s->ext.ech.attempted_cid == OSSL_ECH_config_id_unset)
BIO_printf(out, "ech_atttempted_cid is unset\n");
else
BIO_printf(out, "ech_atttempted_cid=0x%02x\n",
s->ext.ech.attempted_cid);
BIO_printf(out, "ech_done=%d\n", s->ext.ech.done);
BIO_printf(out, "ech_grease=%d\n", s->ext.ech.grease);
#ifdef OSSL_ECH_SUPERVERBOSE
BIO_printf(out, "HRR=%d\n", s->hello_retry_request);
#endif
BIO_printf(out, "ech_backend=%d\n", s->ext.ech.backend);
BIO_printf(out, "ech_success=%d\n", s->ext.ech.success);
es = s->ext.ech.es;
if (es == NULL || es->entries == NULL) {
BIO_printf(out, "ECH cfg=NONE\n");
} else {
num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
BIO_printf(out, "%d ECHConfig values loaded\n", num);
for (i = 0; i != num; i++) {
if (selector != OSSL_ECHSTORE_ALL && selector != i)
continue;
BIO_printf(out, "cfg(%d): ", i);
if (OSSL_ECHSTORE_get1_info(es, i, &secs, &pn, &ec,
&has_priv, &for_retry)
!= 1) {
OPENSSL_free(pn); /* just in case */
OPENSSL_free(ec);
continue;
}
BIO_printf(out, "ECH entry: %d public_name: %s age: %lld%s\n",
i, pn, (long long)secs, has_priv ? " (has private key)" : "");
BIO_printf(out, "\t%s\n", ec);
OPENSSL_free(pn);
OPENSSL_free(ec);
}
}
if (s->ext.ech.returned) {
BIO_printf(out, "ret=");
for (j = 0; j != s->ext.ech.returned_len; j++) {
if (j != 0 && j % 16 == 0)
BIO_printf(out, "\n ");
BIO_printf(out, "%02x:", (unsigned)(s->ext.ech.returned[j]));
}
BIO_printf(out, "\n");
}
return;
}
/*
* Swap the inner and outer after ECH success on the client
* return 0 for error, 1 for success
*/
int ossl_ech_swaperoo(SSL_CONNECTION *s)
{
unsigned char *curr_buf = NULL;
size_t curr_buflen = 0;
if (s == NULL)
return 0;
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_ptranscript(s, "ech_swaperoo, b4");
#endif
/* un-stash inner key share(s) */
if (ossl_ech_unstash_keyshares(s) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
/*
* When not doing HRR... fix up the transcript to reflect the inner CH.
* If there's a client hello at the start of the buffer, then that's
* the outer CH and we want to replace that with the inner. We need to
* be careful that there could be early data or a server hello following
* and we can't lose that.
*
* For HRR... HRR processing code has already done the necessary.
*/
if (s->hello_retry_request == SSL_HRR_NONE) {
BIO *handbuf = s->s3.handshake_buffer;
PACKET pkt, subpkt;
unsigned int mt;
s->s3.handshake_buffer = NULL;
if (ssl3_init_finished_mac(s) == 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
BIO_free(handbuf);
return 0;
}
if (ssl3_finish_mac(s, s->ext.ech.innerch, s->ext.ech.innerch_len) == 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
BIO_free(handbuf);
return 0;
}
curr_buflen = BIO_get_mem_data(handbuf, &curr_buf);
if (PACKET_buf_init(&pkt, curr_buf, curr_buflen)
&& PACKET_get_1(&pkt, &mt)
&& mt == SSL3_MT_CLIENT_HELLO
&& PACKET_remaining(&pkt) >= 3) {
if (!PACKET_get_length_prefixed_3(&pkt, &subpkt)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
BIO_free(handbuf);
return 0;
}
if (PACKET_remaining(&pkt) > 0) {
if (ssl3_finish_mac(s, PACKET_data(&pkt), PACKET_remaining(&pkt)) == 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
BIO_free(handbuf);
return 0;
}
}
BIO_free(handbuf);
}
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_ptranscript(s, "ech_swaperoo, after");
#endif
/* Declare victory! */
s->ext.ech.attempted = 1;
s->ext.ech.success = 1;
s->ext.ech.done = 1;
s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
/* time to call an ECH callback, if there's one */
if (s->ext.ech.es != NULL && s->ext.ech.done == 1
&& s->hello_retry_request != SSL_HRR_PENDING
&& s->ext.ech.cb != NULL) {
char pstr[OSSL_ECH_PBUF_SIZE + 1] = { 0 };
BIO *biom = BIO_new(BIO_s_mem());
unsigned int cbrv = 0;
if (biom == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
ossl_ech_status_print(biom, s, OSSL_ECHSTORE_ALL);
BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE);
cbrv = s->ext.ech.cb(&s->ssl, pstr);
BIO_free(biom);
if (cbrv != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
}
return 1;
}
/*
* do the HKDF for ECH acceptance checking
* md is the h/s hash
* for_hrr is 1 if we're doing a HRR
* hashval/hashlen is the transcript hash
* hoval is the output, with the ECH acceptance signal
* return 1 for good, 0 for error
*/
static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr,
unsigned char *hashval, size_t hashlen,
unsigned char hoval[OSSL_ECH_SIGNAL_LEN])
{
int rv = 0;
unsigned char notsecret[EVP_MAX_MD_SIZE], zeros[EVP_MAX_MD_SIZE];
size_t retlen = 0, labellen = 0;
EVP_PKEY_CTX *pctx = NULL;
const char *label = NULL;
unsigned char *p = NULL;
if (for_hrr == 1) {
label = OSSL_ECH_HRR_CONFIRM_STRING;
labellen = sizeof(OSSL_ECH_HRR_CONFIRM_STRING) - 1;
} else {
label = OSSL_ECH_ACCEPT_CONFIRM_STRING;
labellen = sizeof(OSSL_ECH_ACCEPT_CONFIRM_STRING) - 1;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cc: label", (unsigned char *)label, labellen);
#endif
memset(zeros, 0, EVP_MAX_MD_SIZE);
/* We don't seem to have an hkdf-extract that's exposed by libcrypto */
pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL);
if (pctx == NULL
|| EVP_PKEY_derive_init(pctx) != 1
|| EVP_PKEY_CTX_hkdf_mode(pctx,
EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY)
!= 1
|| EVP_PKEY_CTX_set_hkdf_md(pctx, md) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* pick correct client_random */
if (s->server)
p = s->s3.client_random;
else
p = s->ext.ech.client_random;
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cc: client_random", p, SSL3_RANDOM_SIZE);
#endif
if (EVP_PKEY_CTX_set1_hkdf_key(pctx, p, SSL3_RANDOM_SIZE) != 1
|| EVP_PKEY_CTX_set1_hkdf_salt(pctx, zeros, (int)hashlen) != 1
|| EVP_PKEY_derive(pctx, NULL, &retlen) != 1
|| hashlen != retlen
|| EVP_PKEY_derive(pctx, notsecret, &retlen) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cc: notsecret", notsecret, hashlen);
#endif
if (hashlen < OSSL_ECH_SIGNAL_LEN
|| !tls13_hkdf_expand(s, md, notsecret,
(const unsigned char *)label, labellen,
hashval, hashlen, hoval,
OSSL_ECH_SIGNAL_LEN, 1)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
rv = 1;
err:
EVP_PKEY_CTX_free(pctx);
return rv;
}
/*
* ECH accept_confirmation calculation
* for_hrr is 1 if this is for an HRR, otherwise for SH
* acbuf is an 8 octet buffer for the confirmation value
* shlen is the server hello length
* return: 1 for success, 0 otherwise
*
* This is a magic value in the ServerHello.random lower 8 octets
* that is used to signal that the inner worked.
*
* As per spec:
*
* accept_confirmation = HKDF-Expand-Label(
* HKDF-Extract(0, ClientHelloInner.random),
* "ech accept confirmation",
* transcript_ech_conf,
* 8)
*
* transcript_ech_conf = ClientHelloInner..ServerHello
* with last 8 octets of ServerHello.random==0x00
*
* and with differences due to HRR
*/
int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
unsigned char acbuf[OSSL_ECH_SIGNAL_LEN],
const size_t shlen)
{
int rv = 0;
EVP_MD_CTX *ctx = NULL;
EVP_MD *md = NULL;
unsigned char *tbuf = NULL, *conf_loc = NULL;
unsigned char *fixedshbuf = NULL;
size_t fixedshbuf_len = 0, tlen = 0, chend = 0;
/* shoffset is: 4 + 2 + 32 - 8 */
size_t shoffset = SSL3_HM_HEADER_LENGTH + sizeof(uint16_t)
+ SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN;
unsigned int hashlen = 0;
unsigned char hashval[EVP_MAX_MD_SIZE];
SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
goto end;
}
if (ossl_ech_intbuf_fetch(s, &tbuf, &tlen) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
goto end;
}
chend = tlen - shlen - 4;
fixedshbuf_len = shlen + 4;
if (s->server) {
chend = tlen - shlen;
fixedshbuf_len = shlen;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cx: tbuf b4-b4", tbuf, tlen);
#endif
/* put zeros in correct place */
if (for_hrr == 0) { /* zap magic octets at fixed place for SH */
conf_loc = tbuf + chend + shoffset;
} else {
if (s->server == 1) { /* we get to say where we put ECH:-) */
conf_loc = tbuf + tlen - OSSL_ECH_SIGNAL_LEN;
} else {
if (s->ext.ech.hrrsignal_p == NULL) {
/* No ECH found so we'll exit, but set random output */
if (RAND_bytes_ex(sctx->libctx, acbuf,
OSSL_ECH_SIGNAL_LEN, 0)
<= 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
goto end;
}
rv = 1;
goto end;
}
conf_loc = s->ext.ech.hrrsignal_p;
}
}
memset(conf_loc, 0, OSSL_ECH_SIGNAL_LEN);
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cx: tbuf after", tbuf, tlen);
#endif
if ((ctx = EVP_MD_CTX_new()) == NULL
|| EVP_DigestInit_ex(ctx, md, NULL) <= 0
|| EVP_DigestUpdate(ctx, tbuf, tlen) <= 0
|| EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto end;
}
EVP_MD_CTX_free(ctx);
ctx = NULL;
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cx: hashval", hashval, hashlen);
#endif
/* calculate and set the final output */
if (ech_hkdf_extract_wrap(s, md, for_hrr, hashval, hashlen, acbuf) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto end;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("cx: result", acbuf, OSSL_ECH_SIGNAL_LEN);
#endif
/* put confirm value back into transcript */
memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN);
/* on a server, we need to reset the hs buffer now */
if (s->server && s->hello_retry_request == SSL_HRR_NONE)
ossl_ech_reset_hs_buffer(s, s->ext.ech.innerch, s->ext.ech.innerch_len);
if (s->server && s->hello_retry_request == SSL_HRR_COMPLETE)
ossl_ech_reset_hs_buffer(s, tbuf, tlen - fixedshbuf_len);
rv = 1;
end:
OPENSSL_free(fixedshbuf);
EVP_MD_CTX_free(ctx);
return rv;
}
/*!
* Given a CH find the offsets of the session id, extensions and ECH
* pkt is the CH
* sessid_off points to offset of session_id length
* exts_off points to offset of extensions
* ech_off points to offset of ECH
* echtype points to the ext type of the ECH
* inner 1 if the ECH is marked as an inner, 0 for outer
* sni_off points to offset of (outer) SNI
* return 1 for success, other otherwise
*
* Offsets are set to zero if relevant thing not found.
* Offsets are returned to the type or length field in question.
*
* Note: input here is untrusted!
*/
int ossl_ech_get_ch_offsets(SSL_CONNECTION *s, PACKET *pkt, size_t *sessid_off,
size_t *exts_off, size_t *ech_off, uint16_t *echtype,
int *inner, size_t *sni_off)
{
const unsigned char *ch = NULL;
size_t ch_len = 0, exts_len = 0, sni_len = 0, ech_len = 0;
if (s == NULL)
return 0;
if (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)) {
OPENSSL_free(tmpenc);
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 == 0 || 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 = { 0 }; /* "fake" pkt for inner */
if (encoded_inner == NULL || ob == NULL || ob_len == 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
if ((di_mem = BUF_MEM_new()) == NULL
|| !BUF_MEM_grow(di_mem, SSL3_RT_MAX_PLAIN_LENGTH)
|| !WPACKET_init(&di, di_mem)
|| !WPACKET_put_bytes_u8(&di, SSL3_MT_CLIENT_HELLO)
|| !WPACKET_start_sub_packet_u24(&di)
|| !PACKET_buf_init(&ei, encoded_inner, encoded_inner_len)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
#ifdef OSSL_ECH_SUPERVERBOSE
memset(outers, -1, sizeof(outers)); /* fill with known values for debug */
#endif
/* 1. check for outers and make initial checks of those */
if (ech_find_outers(s, &ei, outers, &n_outers) != 1)
goto err; /* SSLfatal called already */
/* 2. reconstitute inner CH */
/* reset ei */
if (PACKET_buf_init(&ei, encoded_inner, encoded_inner_len) != 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
if (ech_reconstitute_inner(s, &di, &ei, ob, ob_len, outers, n_outers) != 1)
goto err; /* SSLfatal called already */
/* 3. store final inner CH in connection */
WPACKET_close(&di);
if (!WPACKET_get_length(&di, &s->ext.ech.innerch_len)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
OPENSSL_free(s->ext.ech.innerch);
s->ext.ech.innerch = (unsigned char *)di_mem->data;
di_mem->data = NULL;
rv = 1;
err:
WPACKET_cleanup(&di);
BUF_MEM_free(di_mem);
return rv;
}
/*
* wrapper for hpke_dec just to save code repetition
* ee is the selected ECH_STORE entry
* the_ech is the value sent by the client
* aad_len is the length of the AAD to use
* aad is the AAD to use
* forhrr is 0 if not hrr, 1 if this is for 2nd CH
* innerlen points to the size of the recovered plaintext
* return pointer to plaintext or NULL (if error)
*
* The plaintext returned is allocated here and must
* be freed by the caller later.
*/
static unsigned char *hpke_decrypt_encch(SSL_CONNECTION *s,
OSSL_ECHSTORE_ENTRY *ee,
OSSL_ECH_ENCCH *the_ech,
size_t aad_len, unsigned char *aad,
int forhrr, size_t *innerlen)
{
size_t cipherlen = 0;
unsigned char *cipher = NULL;
size_t senderpublen = 0;
unsigned char *senderpub = NULL;
size_t clearlen = 0;
unsigned char *clear = NULL;
int hpke_mode = OSSL_HPKE_MODE_BASE;
OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
unsigned char info[OSSL_ECH_MAX_INFO_LEN];
size_t info_len = OSSL_ECH_MAX_INFO_LEN;
int rv = 0;
OSSL_HPKE_CTX *hctx = NULL;
SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
#ifdef OSSL_ECH_SUPERVERBOSE
size_t publen = 0;
unsigned char *pub = NULL;
#endif
if (ee == NULL || ee->nsuites == 0)
return NULL;
cipherlen = the_ech->payload_len;
cipher = the_ech->payload;
senderpublen = the_ech->enc_len;
senderpub = the_ech->enc;
hpke_suite.aead_id = the_ech->aead_id;
hpke_suite.kdf_id = the_ech->kdf_id;
clearlen = cipherlen; /* small overestimate */
clear = OPENSSL_malloc(clearlen);
if (clear == NULL)
return NULL;
/* The kem_id will be the same for all suites in the entry */
hpke_suite.kem_id = ee->suites[0].kem_id;
#ifdef OSSL_ECH_SUPERVERBOSE
publen = ee->pub_len;
pub = ee->pub;
ossl_ech_pbuf("aad", aad, aad_len);
ossl_ech_pbuf("my local pub", pub, publen);
ossl_ech_pbuf("senderpub", senderpub, senderpublen);
ossl_ech_pbuf("cipher", cipher, cipherlen);
#endif
if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len,
info, &info_len)
!= 1) {
OPENSSL_free(clear);
return NULL;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("info", info, info_len);
#endif
OSSL_TRACE_BEGIN(TLS)
{
BIO_printf(trc_out,
"hpke_dec suite: kem: %04x, kdf: %04x, aead: %04x\n",
hpke_suite.kem_id, hpke_suite.kdf_id, hpke_suite.aead_id);
}
OSSL_TRACE_END(TLS);
/*
* We may generate externally visible OpenSSL errors
* if decryption fails (which is normal) but we'll
* ignore those as we might be dealing with a GREASEd
* ECH. To do that we need to now ignore some errors
* so we use ERR_set_mark() then later ERR_pop_to_mark().
*/
ERR_set_mark();
/* Use OSSL_HPKE_* APIs */
hctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, OSSL_HPKE_ROLE_RECEIVER,
sctx->libctx, sctx->propq);
if (hctx == NULL)
goto clearerrs;
rv = OSSL_HPKE_decap(hctx, senderpub, senderpublen, ee->keyshare,
info, info_len);
if (rv != 1)
goto clearerrs;
if (forhrr == 1) {
rv = OSSL_HPKE_CTX_set_seq(hctx, 1);
if (rv != 1) {
/* don't clear this error - GREASE can't cause it */
ERR_clear_last_mark();
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto end;
}
}
rv = OSSL_HPKE_open(hctx, clear, &clearlen, aad, aad_len,
cipher, cipherlen);
clearerrs:
/* close off our error handling */
ERR_pop_to_mark();
end:
OSSL_HPKE_CTX_free(hctx);
if (rv != 1) {
OSSL_TRACE(TLS, "HPKE decryption failed somehow\n");
OPENSSL_free(clear);
return NULL;
}
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("padded clear", clear, clearlen);
#endif
/* we need to remove possible (actually, v. likely) padding */
*innerlen = clearlen;
if (ee->version == OSSL_ECH_RFC9849_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 + 2) > 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 */
s->ext.ech.grease = OSSL_ECH_IS_GREASE; /* if decrypt fails tee-up GREASE */
s->ext.ech.success = 0;
if (clear != NULL) {
s->ext.ech.grease = OSSL_ECH_NOT_GREASE;
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
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);
clear = NULL;
#ifdef OSSL_ECH_SUPERVERBOSE
ossl_ech_pbuf("Inner CH (decoded)", s->ext.ech.innerch,
s->ext.ech.innerch_len);
#endif
if (PACKET_buf_init(newpkt, s->ext.ech.innerch,
s->ext.ech.innerch_len)
!= 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
/* tls_process_client_hello doesn't want the message header, so skip it */
if (!PACKET_forward(newpkt, SSL3_HM_HEADER_LENGTH)) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
if (ossl_ech_intbuf_add(s, s->ext.ech.innerch,
s->ext.ech.innerch_len, 0)
!= 1) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
goto err;
}
return 1;
err:
OPENSSL_free(aad);
if (extval != NULL) {
ossl_ech_encch_free(extval);
OPENSSL_free(extval);
}
OPENSSL_free(clear);
return 0;
}
int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf,
size_t blen, int hash_existing)
{
EVP_MD_CTX *ctx = NULL;
EVP_MD *md = NULL;
unsigned int rv = 0, hashlen = 0;
unsigned char hashval[EVP_MAX_MD_SIZE], *t1;
size_t tlen;
WPACKET tpkt = { 0 };
BUF_MEM *tpkt_mem = NULL;
if (s == NULL || buf == NULL || blen == 0)
goto err;
if (hash_existing == 1) {
/* hash existing buffer, needed during HRR */
if (s->ext.ech.transbuf == NULL
|| (md = (EVP_MD *)ssl_handshake_md(s)) == NULL
|| (ctx = EVP_MD_CTX_new()) == NULL
|| EVP_DigestInit_ex(ctx, md, NULL) <= 0
|| EVP_DigestUpdate(ctx, s->ext.ech.transbuf,
s->ext.ech.transbuf_len)
<= 0
|| EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0
|| (tpkt_mem = BUF_MEM_new()) == NULL
|| !WPACKET_init(&tpkt, tpkt_mem)
|| !WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH)
|| !WPACKET_put_bytes_u24(&tpkt, hashlen)
|| !WPACKET_memcpy(&tpkt, hashval, hashlen)
|| !WPACKET_get_length(&tpkt, &tlen)
|| (t1 = OPENSSL_realloc(s->ext.ech.transbuf, tlen + blen)) == NULL)
goto err;
s->ext.ech.transbuf = t1;
memcpy(s->ext.ech.transbuf, tpkt_mem->data, tlen);
memcpy(s->ext.ech.transbuf + tlen, buf, blen);
s->ext.ech.transbuf_len = tlen + blen;
} else {
/* just add new octets */
if ((t1 = OPENSSL_realloc(s->ext.ech.transbuf,
s->ext.ech.transbuf_len + blen))
== NULL)
goto err;
s->ext.ech.transbuf = t1;
memcpy(s->ext.ech.transbuf + s->ext.ech.transbuf_len, buf, blen);
s->ext.ech.transbuf_len += blen;
}
rv = 1;
err:
BUF_MEM_free(tpkt_mem);
WPACKET_cleanup(&tpkt);
EVP_MD_CTX_free(ctx);
return rv;
}
int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen)
{
if (s == NULL || buf == NULL || blen == NULL || s->ext.ech.transbuf == NULL)
return 0;
*buf = s->ext.ech.transbuf;
*blen = s->ext.ech.transbuf_len;
return 1;
}
int ossl_ech_stash_keyshares(SSL_CONNECTION *s)
{
size_t i;
ech_free_stashed_key_shares(&s->ext.ech);
for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) {
s->ext.ech.ks_pkey[i] = s->s3.tmp.ks_pkey[i];
if (EVP_PKEY_up_ref(s->ext.ech.ks_pkey[i]) != 1)
return 0;
s->ext.ech.ks_group_id[i] = s->s3.tmp.ks_group_id[i];
}
s->ext.ech.num_ks_pkey = s->s3.tmp.num_ks_pkey;
return 1;
}
int ossl_ech_unstash_keyshares(SSL_CONNECTION *s)
{
size_t i;
for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) {
EVP_PKEY_free(s->s3.tmp.ks_pkey[i]);
s->s3.tmp.ks_pkey[i] = NULL;
}
for (i = 0; i != s->ext.ech.num_ks_pkey; i++) {
s->s3.tmp.ks_pkey[i] = s->ext.ech.ks_pkey[i];
if (EVP_PKEY_up_ref(s->s3.tmp.ks_pkey[i]) != 1)
return 0;
s->s3.tmp.ks_group_id[i] = s->ext.ech.ks_group_id[i];
}
s->s3.tmp.num_ks_pkey = s->ext.ech.num_ks_pkey;
ech_free_stashed_key_shares(&s->ext.ech);
return 1;
}
#endif