blob: fb0b72bde8dad9f768754bb14045994dfe999552 [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 "../ssl_local.h"
#include "ech_local.h"
#include <openssl/rand.h>
#include <openssl/evp.h>
#include <openssl/core_names.h>
/* a size for some crypto vars */
#define OSSL_ECH_CRYPTO_VAR_SIZE 2048
/*
* Used for ech_bio2buf, when reading from a BIO we allocate in chunks sized
* as per below, with a max number of chunks as indicated, we don't expect to
* go beyond one chunk in almost all cases
*/
#define OSSL_ECH_BUFCHUNK 512
#define OSSL_ECH_MAXITER 32
/*
* ECHConfigList input to OSSL_ECHSTORE_read_echconfiglist()
* can be either binary encoded ECHConfigList or a base64
* encoded ECHConfigList.
*/
#define OSSL_ECH_FMT_BIN 1 /* binary ECHConfigList */
#define OSSL_ECH_FMT_B64TXT 2 /* base64 ECHConfigList */
/*
* Telltales we use when guessing which form of encoded input we've
* been given for an RR value or ECHConfig.
* We give these the EBCDIC treatment as well - why not? :-)
*/
static const char B64_alphabet[] = "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52"
"\x53\x54\x55\x56\x57\x58\x59\x5a\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a"
"\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x30\x31"
"\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d\x3b";
#ifndef TLSEXT_MINLEN_host_name
/* The shortest DNS name we allow, e.g. "a.bc" */
#define TLSEXT_MINLEN_host_name 4
#endif
/*
* local functions - public APIs are at the end
*/
void ossl_echext_free(OSSL_ECHEXT *e)
{
if (e == NULL)
return;
OPENSSL_free(e->val);
OPENSSL_free(e);
return;
}
OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src)
{
OSSL_ECHEXT *ext = OPENSSL_zalloc(sizeof(*src));
if (ext == NULL)
return NULL;
*ext = *src;
ext->val = NULL;
if (ext->len != 0) {
ext->val = OPENSSL_memdup(src->val, src->len);
if (ext->val == NULL) {
ossl_echext_free(ext);
return NULL;
}
}
return ext;
}
void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee)
{
if (ee == NULL)
return;
OPENSSL_free(ee->public_name);
OPENSSL_free(ee->pub);
EVP_PKEY_free(ee->keyshare);
OPENSSL_free(ee->encoded);
OPENSSL_free(ee->suites);
sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free);
OPENSSL_free(ee);
return;
}
/*
* @brief Read a buffer from an input 'till eof
* @param in is the BIO input
* @param buf is where to put the buffer, allocated inside here
* @param len is the length of that buffer
*
* This is intended for small inputs, either files or buffers and
* not other kinds of BIO.
*/
static int ech_bio2buf(BIO *in, unsigned char **buf, size_t *len)
{
unsigned char *lptr = NULL, *lbuf = NULL, *tmp = NULL;
size_t sofar = 0, readbytes = 0;
int done = 0, brv, iter = 0;
if (buf == NULL || len == NULL)
return 0;
sofar = OSSL_ECH_BUFCHUNK;
lbuf = OPENSSL_zalloc(sofar);
if (lbuf == NULL)
return 0;
lptr = lbuf;
while (!BIO_eof(in) && !done && iter++ < OSSL_ECH_MAXITER) {
brv = BIO_read_ex(in, lptr, OSSL_ECH_BUFCHUNK, &readbytes);
if (brv != 1)
goto err;
if (BIO_eof(in) || readbytes < OSSL_ECH_BUFCHUNK) {
done = 1;
break;
}
sofar += OSSL_ECH_BUFCHUNK;
tmp = OPENSSL_realloc(lbuf, sofar);
if (tmp == NULL)
goto err;
lbuf = tmp;
lptr = lbuf + sofar - OSSL_ECH_BUFCHUNK;
}
if (BIO_eof(in) && done == 1) {
*len = sofar + readbytes - OSSL_ECH_BUFCHUNK;
*buf = lbuf;
return 1;
}
err:
OPENSSL_free(lbuf);
return 0;
}
/*
* @brief Figure out ECHConfig encoding
* @param val is a buffer with the encoding
* @param len is the length of that buffer
* @param fmt is the detected format
* @return 1 for success, 0 for error
*/
static int ech_check_format(const unsigned char *val, size_t len, int *fmt)
{
size_t span = 0;
char *copy_with_NUL = NULL;
if (fmt == NULL || len <= 4 || val == NULL)
return 0;
/* binary encoding starts with two octet length and ECH version */
if (len == 2 + ((size_t)(val[0]) * 256 + (size_t)(val[1]))
&& val[2] == ((OSSL_ECH_RFC9849_VERSION / 256) & 0xff)
&& val[3] == ((OSSL_ECH_RFC9849_VERSION % 256) & 0xff)) {
*fmt = OSSL_ECH_FMT_BIN;
return 1;
}
/* ensure we always end with a NUL so strspn is safe */
copy_with_NUL = OPENSSL_malloc(len + 1);
if (copy_with_NUL == NULL)
return 0;
memcpy(copy_with_NUL, val, len);
copy_with_NUL[len] = '\0';
span = strspn(copy_with_NUL, B64_alphabet);
OPENSSL_free(copy_with_NUL);
if (len <= span) {
*fmt = OSSL_ECH_FMT_B64TXT;
return 1;
}
return 0;
}
/*
* @brief helper to decode ECHConfig extensions
* @param ee is the OSSL_ECHSTORE entry for these
* @param exts is the binary form extensions
* @return 1 for good, 0 for error
*/
static int ech_decode_echconfig_exts(OSSL_ECHSTORE_ENTRY *ee, PACKET *exts)
{
unsigned int exttype = 0;
size_t extlen = 0;
unsigned char *extval = NULL;
OSSL_ECHEXT *oe = NULL;
PACKET ext;
/*
* reminder: exts is a two-octet length prefixed list of:
* - two octet extension type
* - two octet extension length (can be zero)
* - length octets
* we've consumed the overall length before getting here
*/
while (PACKET_remaining(exts) > 0) {
exttype = 0, extlen = 0;
extval = NULL;
oe = NULL;
if (!PACKET_get_net_2(exts, &exttype) || !PACKET_get_length_prefixed_2(exts, &ext)) {
ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
goto err;
}
if (PACKET_remaining(&ext) >= OSSL_ECH_MAX_ECHCONFIGEXT_LEN) {
ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
goto err;
}
if (!PACKET_memdup(&ext, &extval, &extlen)) {
ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION);
goto err;
}
oe = OPENSSL_malloc(sizeof(*oe));
if (oe == NULL)
goto err;
oe->type = (uint16_t)exttype;
oe->val = extval;
extval = NULL; /* avoid double free */
oe->len = (uint16_t)extlen;
if (ee->exts == NULL)
ee->exts = sk_OSSL_ECHEXT_new_null();
if (ee->exts == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (!sk_OSSL_ECHEXT_push(ee->exts, oe)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
}
return 1;
err:
sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free);
ee->exts = NULL;
ossl_echext_free(oe);
OPENSSL_free(extval);
return 0;
}
/*
* @brief Check entry to see if looks good or bad
* @param ee is the ECHConfig to check
* @return 1 for all good, 0 otherwise
*/
static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee)
{
OSSL_HPKE_SUITE hpke_suite;
int ind, num;
int goodsuitefound = 0;
/* check local support for some suite */
for (ind = 0; ind != (int)ee->nsuites; ind++) {
/*
* suite_check says yes to the pseudo-aead for export, but we don't
* want to see it here coming from outside in an encoding
*/
hpke_suite = ee->suites[ind];
if (OSSL_HPKE_suite_check(hpke_suite) == 1
&& hpke_suite.aead_id != OSSL_HPKE_AEAD_ID_EXPORTONLY) {
goodsuitefound = 1;
break;
}
}
if (goodsuitefound == 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
/* check no mandatory exts (with high bit set in type) */
num = (ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
for (ind = 0; ind != num; ind++) {
OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, (int)ind);
if (oe->type & 0x8000) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
}
/* check public_name rules, as per spec section 4 */
if (ee->public_name == NULL
|| ee->public_name[0] == '\0'
|| ee->public_name[0] == '.'
|| ee->public_name[strlen(ee->public_name) - 1] == '.')
return 0;
return 1;
}
/**
* @brief decode one ECHConfig from a packet into an entry
* @param rent ptr to an entry allocated within (on success)
* @param pkt is the encoding
* @param priv is an optional private key (NULL if absent)
* @param for_retry says whether to include in a retry_config (if priv present)
* @return 1 for success, 0 for error
*/
static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt,
EVP_PKEY *priv, int for_retry)
{
size_t ech_content_length = 0;
unsigned int tmpi;
const unsigned char *tmpecp = NULL;
size_t tmpeclen = 0, test_publen = 0;
PACKET ver_pkt, pub_pkt, cipher_suites, public_name_pkt, exts;
uint16_t thiskemid;
size_t suiteoctets = 0;
unsigned int ci = 0;
unsigned char cipher[OSSL_ECH_CIPHER_LEN], max_name_len;
unsigned char test_pub[OSSL_ECH_CRYPTO_VAR_SIZE];
OSSL_ECHSTORE_ENTRY *ee = NULL;
if (rent == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
return 0;
}
if (pkt == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
ee = OPENSSL_zalloc(sizeof(*ee));
if (ee == NULL)
goto err;
/* note start of encoding so we can make a copy later */
tmpeclen = PACKET_remaining(pkt);
if (PACKET_peek_bytes(pkt, &tmpecp, tmpeclen) != 1
|| !PACKET_get_net_2(pkt, &tmpi)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
ee->version = (uint16_t)tmpi;
/* grab versioned packet data */
if (!PACKET_get_length_prefixed_2(pkt, &ver_pkt)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
ech_content_length = (unsigned int)PACKET_remaining(&ver_pkt);
switch (ee->version) {
case OSSL_ECH_RFC9849_VERSION:
break;
default:
/* skip over in case we get something we can handle later */
if (!PACKET_forward(&ver_pkt, ech_content_length)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
/* nothing to return but not a fail */
ossl_echstore_entry_free(ee);
*rent = NULL;
return 1;
}
if (!PACKET_copy_bytes(&ver_pkt, &ee->config_id, 1)
|| !PACKET_get_net_2(&ver_pkt, &tmpi)
|| !PACKET_get_length_prefixed_2(&ver_pkt, &pub_pkt)
|| !PACKET_memdup(&pub_pkt, &ee->pub, &ee->pub_len)
|| !PACKET_get_length_prefixed_2(&ver_pkt, &cipher_suites)
|| (suiteoctets = PACKET_remaining(&cipher_suites)) <= 0
|| (suiteoctets % 2) == 1
|| suiteoctets / OSSL_ECH_CIPHER_LEN > UINT_MAX) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
thiskemid = (uint16_t)tmpi;
ee->nsuites = (unsigned int)(suiteoctets / OSSL_ECH_CIPHER_LEN);
ee->suites = OPENSSL_malloc_array(ee->nsuites, sizeof(*ee->suites));
if (ee->suites == NULL)
goto err;
while (PACKET_copy_bytes(&cipher_suites, cipher,
OSSL_ECH_CIPHER_LEN)) {
ee->suites[ci].kem_id = thiskemid;
ee->suites[ci].kdf_id = cipher[0] << 8 | cipher[1];
ee->suites[ci].aead_id = cipher[2] << 8 | cipher[3];
if (ci++ >= ee->nsuites) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
}
if (PACKET_remaining(&cipher_suites) > 0
|| !PACKET_copy_bytes(&ver_pkt, &max_name_len, 1)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
ee->max_name_length = max_name_len;
if (!PACKET_get_length_prefixed_1(&ver_pkt, &public_name_pkt)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
if (PACKET_contains_zero_byte(&public_name_pkt)
|| PACKET_remaining(&public_name_pkt) < TLSEXT_MINLEN_host_name
|| !PACKET_strndup(&public_name_pkt, &ee->public_name)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
if (!PACKET_get_length_prefixed_2(&ver_pkt, &exts)) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
if (PACKET_remaining(&exts) > 0
&& ech_decode_echconfig_exts(ee, &exts) != 1) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
/* set length of encoding of this ECHConfig */
ee->encoded_len = PACKET_data(&ver_pkt) - tmpecp;
/* copy encoded as it might get free'd if a reduce happens */
ee->encoded = OPENSSL_memdup(tmpecp, ee->encoded_len);
if (ee->encoded == NULL)
goto err;
if (priv != NULL) {
if (EVP_PKEY_get_octet_string_param(priv,
OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY,
test_pub, OSSL_ECH_CRYPTO_VAR_SIZE,
&test_publen)
!= 1) {
ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
goto err;
}
if (test_publen == ee->pub_len
&& !memcmp(test_pub, ee->pub, ee->pub_len)) {
EVP_PKEY_up_ref(priv); /* associate the private key */
ee->keyshare = priv;
ee->for_retry = for_retry;
}
}
ee->loadtime = time(0);
*rent = ee;
return 1;
err:
ossl_echstore_entry_free(ee);
*rent = NULL;
return 0;
}
/*
* @brief decode and flatten a binary encoded ECHConfigList
* @param es an OSSL_ECHSTORE
* @param priv is an optional private key (NULL if absent)
* @param for_retry says whether to include in a retry_config (if priv present)
* @param binbuf binary encoded ECHConfigList (we hope)
* @param binlen length of binbuf
* @return 1 for success, 0 for error
*
* We may only get one ECHConfig per list, but there can be more. We want each
* element of the output to contain exactly one ECHConfig so that a client
* could sensibly down select to the one they prefer later, and so that we have
* the specific encoded value of that ECHConfig for inclusion in the HPKE info
* parameter when finally encrypting or decrypting an inner ClientHello.
*
* If a private value is provided then that'll only be associated with the
* relevant public value, if >1 public value was present in the ECHConfigList.
*/
static int ech_decode_and_flatten(OSSL_ECHSTORE *es, EVP_PKEY *priv, int for_retry,
unsigned char *binbuf, size_t binblen)
{
int rv = 0;
size_t remaining = 0;
PACKET opkt, pkt;
OSSL_ECHSTORE_ENTRY *ee = NULL;
if (binbuf == NULL || binblen == 0 || binblen < OSSL_ECH_MIN_ECHCONFIG_LEN
|| binblen >= OSSL_ECH_MAX_ECHCONFIG_LEN) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
goto err;
}
if (PACKET_buf_init(&opkt, binbuf, binblen) != 1
|| !PACKET_get_length_prefixed_2(&opkt, &pkt)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
remaining = PACKET_remaining(&pkt);
while (remaining > 0) {
if (ech_decode_one_entry(&ee, &pkt, priv, for_retry) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
remaining = PACKET_remaining(&pkt);
/* if unsupported version we can skip over */
if (ee == NULL)
continue;
/* do final checks on suites, exts, and fail if issues */
if (ech_final_config_checks(ee) != 1)
goto err;
/* push entry into store */
if (es->entries == NULL)
es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null();
if (es->entries == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
ee = NULL;
}
rv = 1;
err:
ossl_echstore_entry_free(ee);
return rv;
}
/*
* @brief check a private matches some public
* @param es is the ECH store
* @param priv is the private value
* @return 1 if we have a match, zero otherwise
*/
static int check_priv_matches(OSSL_ECHSTORE *es, EVP_PKEY *priv)
{
int num, ent, gotone = 0;
OSSL_ECHSTORE_ENTRY *ee = NULL;
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
for (ent = 0; ent != num; ent++) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, ent);
if (ee == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (EVP_PKEY_eq(ee->keyshare, priv)) {
gotone = 1;
break;
}
}
return gotone;
}
/*
* @brief decode input ECHConfigList and associate optional private info
* @param es is the OSSL_ECHSTORE
* @param in is the BIO from which we'll get the ECHConfigList
* @param priv is an optional private key
* @param for_retry 1 if the public related to priv ought be in retry_config
*/
static int ech_read_priv_echconfiglist(OSSL_ECHSTORE *es, BIO *in,
EVP_PKEY *priv, int for_retry)
{
int rv = 0, detfmt, tdeclen = 0;
size_t encodedlen = 0, binlen = 0;
unsigned char *encodedval = NULL, *binbuf = NULL;
BIO *btmp = NULL, *btmp1 = NULL;
if (es == NULL || in == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
if (ech_bio2buf(in, &encodedval, &encodedlen) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
return 0;
}
if (encodedlen >= OSSL_ECH_MAX_ECHCONFIG_LEN) { /* sanity check */
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (ech_check_format(encodedval, encodedlen, &detfmt) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
goto err;
}
if (detfmt == OSSL_ECH_FMT_BIN) { /* copy buffer if binary format */
binbuf = OPENSSL_memdup(encodedval, encodedlen);
if (binbuf == NULL)
goto err;
binlen = encodedlen;
}
if (detfmt == OSSL_ECH_FMT_B64TXT) {
btmp = BIO_new_mem_buf(encodedval, (int)encodedlen);
if (btmp == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
btmp1 = BIO_new(BIO_f_base64());
if (btmp1 == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
BIO_set_flags(btmp1, BIO_FLAGS_BASE64_NO_NL);
btmp = BIO_push(btmp1, btmp);
/* overestimate but good enough */
binbuf = OPENSSL_malloc(encodedlen);
if (binbuf == NULL)
goto err;
tdeclen = BIO_read(btmp, binbuf, (int)encodedlen);
if (tdeclen <= 0) { /* need int for -1 return in failure case */
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
binlen = tdeclen;
}
if (ech_decode_and_flatten(es, priv, for_retry, binbuf, binlen) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (priv != NULL && check_priv_matches(es, priv) == 0)
goto err;
rv = 1;
err:
BIO_free_all(btmp);
OPENSSL_free(binbuf);
OPENSSL_free(encodedval);
return rv;
}
/*
* API calls built around OSSL_ECHSSTORE
*/
OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq)
{
OSSL_ECHSTORE *es = NULL;
es = OPENSSL_zalloc(sizeof(*es));
if (es == NULL)
return 0;
es->libctx = libctx;
if (propq != NULL) {
es->propq = OPENSSL_strdup(propq);
if (es->propq == NULL) {
OPENSSL_free(es);
return 0;
}
}
return es;
}
void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es)
{
if (es == NULL)
return;
sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free);
OPENSSL_free(es->propq);
OPENSSL_free(es);
return;
}
int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es,
uint16_t echversion, uint8_t max_name_length,
const char *public_name, OSSL_HPKE_SUITE suite)
{
size_t pnlen = 0, publen = OSSL_ECH_CRYPTO_VAR_SIZE;
unsigned char pub[OSSL_ECH_CRYPTO_VAR_SIZE];
int rv = 0;
unsigned char *bp = NULL;
size_t bblen = 0;
EVP_PKEY *privp = NULL;
uint8_t config_id = 0;
WPACKET epkt;
BUF_MEM *epkt_mem = NULL;
OSSL_ECHSTORE_ENTRY *ee = NULL;
/* basic checks */
if (es == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
pnlen = (public_name == NULL ? 0 : strlen(public_name));
if (pnlen == 0 || pnlen > OSSL_ECH_MAX_PUBLICNAME) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
/* this used have more versions and will again in future */
switch (echversion) {
case OSSL_ECH_RFC9849_VERSION:
break;
default:
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
/*
* Reminder, for draft-13 we want this:
*
* opaque HpkePublicKey<1..2^16-1>;
* uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke
* uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke
* uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke
* struct {
* HpkeKdfId kdf_id;
* HpkeAeadId aead_id;
* } HpkeSymmetricCipherSuite;
* struct {
* uint8 config_id;
* HpkeKemId kem_id;
* HpkePublicKey public_key;
* HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>;
* } HpkeKeyConfig;
* struct {
* HpkeKeyConfig key_config;
* uint8 maximum_name_length;
* opaque public_name<1..255>;
* Extension extensions<0..2^16-1>;
* } ECHConfigContents;
* struct {
* uint16 version;
* uint16 length;
* select (ECHConfig.version) {
* case 0xfe0d: ECHConfigContents contents;
* }
* } ECHConfig;
* ECHConfig ECHConfigList<1..2^16-1>;
*/
if ((epkt_mem = BUF_MEM_new()) == NULL
|| !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)
|| !WPACKET_init(&epkt, epkt_mem)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err_no_epkt;
}
/* random config_id */
if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, 0) <= 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
/* key pair */
if (OSSL_HPKE_keygen(suite, pub, &publen, &privp, NULL, 0,
es->libctx, es->propq)
!= 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
/* config id, KEM, public, KDF, AEAD, max name len, public_name, exts */
if ((bp = WPACKET_get_curr(&epkt)) == NULL
|| !WPACKET_start_sub_packet_u16(&epkt)
|| !WPACKET_put_bytes_u16(&epkt, echversion)
|| !WPACKET_start_sub_packet_u16(&epkt)
|| !WPACKET_put_bytes_u8(&epkt, config_id)
|| !WPACKET_put_bytes_u16(&epkt, suite.kem_id)
|| !WPACKET_start_sub_packet_u16(&epkt)
|| !WPACKET_memcpy(&epkt, pub, publen)
|| !WPACKET_close(&epkt)
|| !WPACKET_start_sub_packet_u16(&epkt)
|| !WPACKET_put_bytes_u16(&epkt, suite.kdf_id)
|| !WPACKET_put_bytes_u16(&epkt, suite.aead_id)
|| !WPACKET_close(&epkt)
|| !WPACKET_put_bytes_u8(&epkt, max_name_length)
|| !WPACKET_start_sub_packet_u8(&epkt)
|| !WPACKET_memcpy(&epkt, public_name, pnlen)
|| !WPACKET_close(&epkt)
|| !WPACKET_start_sub_packet_u16(&epkt)
|| !WPACKET_memcpy(&epkt, NULL, 0) /* no extensions */
|| !WPACKET_close(&epkt)
|| !WPACKET_close(&epkt)
|| !WPACKET_close(&epkt)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
/* bp, bblen has encoding */
if (!WPACKET_get_total_written(&epkt, &bblen)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if ((ee = OPENSSL_zalloc(sizeof(*ee))) == NULL)
goto err;
ee->suites = OPENSSL_malloc(sizeof(*ee->suites));
if (ee->suites == NULL)
goto err;
ee->version = echversion;
ee->pub_len = publen;
ee->pub = OPENSSL_memdup(pub, publen);
if (ee->pub == NULL)
goto err;
ee->nsuites = 1;
ee->suites[0] = suite;
ee->public_name = OPENSSL_strdup(public_name);
if (ee->public_name == NULL)
goto err;
ee->max_name_length = max_name_length;
ee->config_id = config_id;
ee->keyshare = privp;
privp = NULL; /* don't free twice */
/* "steal" the encoding from the memory */
ee->encoded = (unsigned char *)epkt_mem->data;
ee->encoded_len = bblen;
epkt_mem->data = NULL;
epkt_mem->length = 0;
ee->loadtime = time(0);
/* push entry into store */
if (es->entries == NULL)
es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null();
if (es->entries == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
WPACKET_finish(&epkt);
BUF_MEM_free(epkt_mem);
return 1;
err:
ossl_echstore_entry_free(ee);
EVP_PKEY_free(privp);
WPACKET_cleanup(&epkt);
err_no_epkt:
BUF_MEM_free(epkt_mem);
return rv;
}
int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out)
{
OSSL_ECHSTORE_ENTRY *ee = NULL;
int rv = 0, num = 0, chosen = 0, doall = 0;
WPACKET epkt; /* used if we want to merge ECHConfigs for output */
BUF_MEM *epkt_mem = NULL;
size_t allencoded_len;
if (es == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
if (num <= 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (index >= num) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (index == OSSL_ECHSTORE_ALL)
doall = 1;
else if (index == OSSL_ECHSTORE_LAST)
chosen = num - 1;
else
chosen = index;
memset(&epkt, 0, sizeof(epkt));
if (doall == 0) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen);
if (ee == NULL || ee->encoded == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
/* private key first */
if (ee->keyshare != NULL
&& !PEM_write_bio_PrivateKey(out, ee->keyshare, NULL, NULL, 0,
NULL, NULL)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL,
ee->encoded, (long)ee->encoded_len)
<= 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
} else {
/* catenate the encodings into one */
if ((epkt_mem = BUF_MEM_new()) == NULL
|| !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)
|| !WPACKET_init(&epkt, epkt_mem)
|| !WPACKET_start_sub_packet_u16(&epkt)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
for (chosen = 0; chosen != num; chosen++) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen);
if (ee == NULL || ee->encoded == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (!WPACKET_memcpy(&epkt, ee->encoded, ee->encoded_len)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
}
if (!WPACKET_close(&epkt)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (!WPACKET_get_total_written(&epkt, &allencoded_len)) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL,
(unsigned char *)epkt_mem->data,
(long)allencoded_len)
<= 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
}
rv = 1;
err:
WPACKET_cleanup(&epkt);
BUF_MEM_free(epkt_mem);
return rv;
}
int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in)
{
return ech_read_priv_echconfiglist(es, in, NULL, 0);
}
int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs,
char **public_name, char **echconfig,
int *has_private, int *for_retry)
{
OSSL_ECHSTORE_ENTRY *ee = NULL;
unsigned int j = 0;
int num = 0;
BIO *out = NULL;
time_t now = time(0);
size_t ehlen;
unsigned char *ignore = NULL;
if (es == NULL || loaded_secs == NULL || public_name == NULL
|| echconfig == NULL || has_private == NULL || for_retry == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
if (num == 0 || index < 0 || index >= num) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, index);
if (ee == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
*loaded_secs = now - ee->loadtime;
*public_name = NULL;
*echconfig = NULL;
if (ee->public_name != NULL) {
*public_name = OPENSSL_strdup(ee->public_name);
if (*public_name == NULL)
goto err;
}
*has_private = (ee->keyshare == NULL ? 0 : 1);
*for_retry = ee->for_retry;
/* Now "print" the ECHConfigList */
out = BIO_new(BIO_s_mem());
if (out == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
if (ee->version != OSSL_ECH_RFC9849_VERSION) {
/* just note we don't support that one today */
BIO_printf(out, "[Unsupported version (%04x)]", ee->version);
} else {
/* version, config_id, public_name, and kem */
BIO_printf(out, "[%04x,%02x,%s,[", ee->version, ee->config_id,
ee->public_name != NULL ? (char *)ee->public_name : "NULL");
/* ciphersuites */
for (j = 0; j != ee->nsuites; j++) {
BIO_printf(out, "%04x,%04x,%04x", ee->suites[j].kem_id,
ee->suites[j].kdf_id, ee->suites[j].aead_id);
if (j < (ee->nsuites - 1))
BIO_printf(out, ",");
}
BIO_printf(out, "],");
/* public key */
for (j = 0; j != ee->pub_len; j++)
BIO_printf(out, "%02x", ee->pub[j]);
/* max name length and (only) number of extensions */
BIO_printf(out, ",%02x,%02x]", ee->max_name_length,
ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
}
ehlen = BIO_get_mem_data(out, &ignore);
if (ehlen > INT_MAX)
goto err;
*echconfig = OPENSSL_malloc(ehlen + 1);
if (*echconfig == NULL)
goto err;
if (BIO_read(out, *echconfig, (int)ehlen) <= 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
(*echconfig)[ehlen] = '\0';
BIO_free(out);
return 1;
err:
BIO_free(out);
OPENSSL_free(*public_name);
*public_name = NULL;
OPENSSL_free(*echconfig);
*echconfig = NULL;
return 0;
}
int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index)
{
OSSL_ECHSTORE_ENTRY *ee = NULL;
int i, num = 0, chosen = OSSL_ECHSTORE_ALL;
if (es == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
if (num == 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (index <= OSSL_ECHSTORE_ALL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (index == OSSL_ECHSTORE_LAST) {
chosen = num - 1;
} else if (index >= num) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
} else {
chosen = index;
}
for (i = num - 1; i >= 0; i--) {
if (i == chosen)
continue;
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
ossl_echstore_entry_free(ee);
sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i);
}
return 1;
}
int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv,
BIO *in, int for_retry)
{
unsigned char *b64 = NULL;
long b64len = 0;
BIO *b64bio = NULL;
int rv = 0;
char *pname = NULL, *pheader = NULL;
/* we allow for a NULL private key */
if (es == NULL || in == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
if (PEM_read_bio(in, &pname, &pheader, &b64, &b64len) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
return 0;
}
if (pname == NULL || strcmp(pname, PEM_STRING_ECHCONFIG) != 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
b64bio = BIO_new(BIO_s_mem());
if (b64bio == NULL
|| BIO_write(b64bio, b64, b64len) <= 0
|| ech_read_priv_echconfiglist(es, b64bio, priv, for_retry) != 1) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
rv = 1;
err:
OPENSSL_free(pname);
OPENSSL_free(pheader);
BIO_free_all(b64bio);
OPENSSL_free(b64);
return rv;
}
int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry)
{
EVP_PKEY *priv = NULL;
int rv = 0;
BIO *fbio = BIO_new(BIO_f_buffer());
if (fbio == NULL || es == NULL || in == NULL) {
BIO_free_all(fbio);
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
/*
* Read private key then handoff to set1_key_and_read_pem.
* We allow for no private key as an option, to handle that
* the BIO_f_buffer allows us to seek back to the start.
*/
BIO_push(fbio, in);
if (!PEM_read_bio_PrivateKey(fbio, &priv, NULL, NULL)
&& BIO_seek(fbio, 0) < 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
goto err;
}
rv = OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, fbio, for_retry);
err:
EVP_PKEY_free(priv);
BIO_pop(fbio);
BIO_free_all(fbio);
return rv;
}
int OSSL_ECHSTORE_num_entries(const OSSL_ECHSTORE *es, int *numentries)
{
if (es == NULL || numentries == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
*numentries = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
return 1;
}
int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys)
{
int i, num = 0, count = 0;
OSSL_ECHSTORE_ENTRY *ee = NULL;
if (es == NULL || numkeys == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
for (i = 0; i != num; i++) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
if (ee == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
return 0;
}
count += (ee->keyshare != NULL);
}
*numkeys = count;
return 1;
}
int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age)
{
OSSL_ECHSTORE_ENTRY *ee = NULL;
int i, num = 0;
time_t now = time(0);
if (es == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER);
return 0;
}
num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries));
if (num == 0) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
for (i = num - 1; i >= 0; i--) {
ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
if (ee == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
return 0;
}
if (ee->keyshare != NULL && ee->loadtime + age <= now) {
ossl_echstore_entry_free(ee);
sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i);
}
}
return 1;
}