diff options
Diffstat (limited to 'crypto/openssl/ssl/quic/quic_srtm.c')
| -rw-r--r-- | crypto/openssl/ssl/quic/quic_srtm.c | 565 |
1 files changed, 565 insertions, 0 deletions
diff --git a/crypto/openssl/ssl/quic/quic_srtm.c b/crypto/openssl/ssl/quic/quic_srtm.c new file mode 100644 index 000000000000..3d0bfd97c7e0 --- /dev/null +++ b/crypto/openssl/ssl/quic/quic_srtm.c @@ -0,0 +1,565 @@ +/* + * Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include "internal/quic_srtm.h" +#include "internal/common.h" +#include <openssl/lhash.h> +#include <openssl/core_names.h> +#include <openssl/rand.h> + +/* + * QUIC Stateless Reset Token Manager + * ================================== + */ +typedef struct srtm_item_st SRTM_ITEM; + +#define BLINDED_SRT_LEN 16 + +DEFINE_LHASH_OF_EX(SRTM_ITEM); + +/* + * The SRTM is implemented using two LHASH instances, one matching opaque pointers to + * an item structure, and another matching a SRT-derived value to an item + * structure. Multiple items with different seq_num values under a given opaque, + * and duplicate SRTs, are handled using sorted singly-linked lists. + * + * The O(n) insert and lookup performance is tolerated on the basis that the + * total number of entries for a given opaque (total number of extant CIDs for a + * connection) should be quite small, and the QUIC protocol allows us to place a + * hard limit on this via the active_connection_id_limit TPARAM. Thus there is + * no risk of a large number of SRTs needing to be registered under a given + * opaque. + * + * It is expected one SRTM will exist per QUIC_PORT and track all SRTs across + * all connections for that QUIC_PORT. + */ +struct srtm_item_st { + SRTM_ITEM *next_by_srt_blinded; /* SORT BY opaque DESC */ + SRTM_ITEM *next_by_seq_num; /* SORT BY seq_num DESC */ + void *opaque; /* \__ unique identity for item */ + uint64_t seq_num; /* / */ + QUIC_STATELESS_RESET_TOKEN srt; + unsigned char srt_blinded[BLINDED_SRT_LEN]; /* H(srt) */ + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + uint32_t debug_token; +#endif +}; + +struct quic_srtm_st { + /* Crypto context used to calculate blinded SRTs H(srt). */ + EVP_CIPHER_CTX *blind_ctx; /* kept with key */ + + LHASH_OF(SRTM_ITEM) *items_fwd; /* (opaque) -> SRTM_ITEM */ + LHASH_OF(SRTM_ITEM) *items_rev; /* (H(srt)) -> SRTM_ITEM */ + + /* + * Monotonically transitions to 1 in event of allocation failure. The only + * valid operation on such an object is to free it. + */ + unsigned int alloc_failed : 1; +}; + +static unsigned long items_fwd_hash(const SRTM_ITEM *item) +{ + return (unsigned long)(uintptr_t)item->opaque; +} + +static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b) +{ + return a->opaque != b->opaque; +} + +static unsigned long items_rev_hash(const SRTM_ITEM *item) +{ + /* + * srt_blinded has already been through a crypto-grade hash function, so we + * can just use bits from that. + */ + unsigned long l; + + memcpy(&l, item->srt_blinded, sizeof(l)); + return l; +} + +static int items_rev_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b) +{ + /* + * We don't need to use CRYPTO_memcmp here as the relationship of + * srt_blinded to srt is already cryptographically obfuscated. + */ + return memcmp(a->srt_blinded, b->srt_blinded, sizeof(a->srt_blinded)); +} + +static int srtm_check_lh(QUIC_SRTM *srtm, LHASH_OF(SRTM_ITEM) *lh) +{ + if (lh_SRTM_ITEM_error(lh)) { + srtm->alloc_failed = 1; + return 0; + } + + return 1; +} + +QUIC_SRTM *ossl_quic_srtm_new(OSSL_LIB_CTX *libctx, const char *propq) +{ + QUIC_SRTM *srtm = NULL; + unsigned char key[16]; + EVP_CIPHER *ecb = NULL; + + if (RAND_priv_bytes_ex(libctx, key, sizeof(key), sizeof(key) * 8) != 1) + goto err; + + if ((srtm = OPENSSL_zalloc(sizeof(*srtm))) == NULL) + return NULL; + + /* Use AES-128-ECB as a permutation over 128-bit SRTs. */ + if ((ecb = EVP_CIPHER_fetch(libctx, "AES-128-ECB", propq)) == NULL) + goto err; + + if ((srtm->blind_ctx = EVP_CIPHER_CTX_new()) == NULL) + goto err; + + if (!EVP_EncryptInit_ex2(srtm->blind_ctx, ecb, key, NULL, NULL)) + goto err; + + EVP_CIPHER_free(ecb); + ecb = NULL; + + /* Create mappings. */ + if ((srtm->items_fwd = lh_SRTM_ITEM_new(items_fwd_hash, items_fwd_cmp)) == NULL + || (srtm->items_rev = lh_SRTM_ITEM_new(items_rev_hash, items_rev_cmp)) == NULL) + goto err; + + return srtm; + +err: + /* + * No cleansing of key needed as blinding exists only for side channel + * mitigation. + */ + ossl_quic_srtm_free(srtm); + EVP_CIPHER_free(ecb); + return NULL; +} + +static void srtm_free_each(SRTM_ITEM *ihead) +{ + SRTM_ITEM *inext, *item = ihead; + + for (item = item->next_by_seq_num; item != NULL; item = inext) { + inext = item->next_by_seq_num; + OPENSSL_free(item); + } + + OPENSSL_free(ihead); +} + +void ossl_quic_srtm_free(QUIC_SRTM *srtm) +{ + if (srtm == NULL) + return; + + lh_SRTM_ITEM_free(srtm->items_rev); + if (srtm->items_fwd != NULL) { + lh_SRTM_ITEM_doall(srtm->items_fwd, srtm_free_each); + lh_SRTM_ITEM_free(srtm->items_fwd); + } + + EVP_CIPHER_CTX_free(srtm->blind_ctx); + OPENSSL_free(srtm); +} + +/* + * Find a SRTM_ITEM by (opaque, seq_num). Returns NULL if no match. + * If head is non-NULL, writes the head of the relevant opaque list to *head if + * there is one. + * If prev is non-NULL, writes the previous node to *prev or NULL if it is + * the first item. + */ +static SRTM_ITEM *srtm_find(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num, + SRTM_ITEM **head_p, SRTM_ITEM **prev_p) +{ + SRTM_ITEM key, *item = NULL, *prev = NULL; + + key.opaque = opaque; + + item = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key); + if (head_p != NULL) + *head_p = item; + + for (; item != NULL; prev = item, item = item->next_by_seq_num) + if (item->seq_num == seq_num) { + break; + } else if (item->seq_num < seq_num) { + /* + * List is sorted in descending order so there can't be any match + * after this. + */ + item = NULL; + break; + } + + if (prev_p != NULL) + *prev_p = prev; + + return item; +} + +/* + * Inserts a SRTM_ITEM into the singly-linked by-sequence-number linked list. + * The new head pointer is written to *new_head (which may or may not be + * unchanged). + */ +static void sorted_insert_seq_num(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head) +{ + uint64_t seq_num = item->seq_num; + SRTM_ITEM *cur = head, **fixup = new_head; + + *new_head = head; + + while (cur != NULL && cur->seq_num > seq_num) { + fixup = &cur->next_by_seq_num; + cur = cur->next_by_seq_num; + } + + item->next_by_seq_num = *fixup; + *fixup = item; +} + +/* + * Inserts a SRTM_ITEM into the singly-linked by-SRT list. + * The new head pointer is written to *new_head (which may or may not be + * unchanged). + */ +static void sorted_insert_srt(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head) +{ + uintptr_t opaque = (uintptr_t)item->opaque; + SRTM_ITEM *cur = head, **fixup = new_head; + + *new_head = head; + + while (cur != NULL && (uintptr_t)cur->opaque > opaque) { + fixup = &cur->next_by_srt_blinded; + cur = cur->next_by_srt_blinded; + } + + item->next_by_srt_blinded = *fixup; + *fixup = item; +} + +/* + * Computes the blinded SRT value used for internal lookup for side channel + * mitigation purposes. We compute this once as a cached value when an SRTM_ITEM + * is formed. + */ +static int srtm_compute_blinded(QUIC_SRTM *srtm, SRTM_ITEM *item, + const QUIC_STATELESS_RESET_TOKEN *token) +{ + int outl = 0; + + /* + * We use AES-128-ECB as a permutation using a random key to facilitate + * blinding for side-channel purposes. Encrypt the token as a single AES + * block. + */ + if (!EVP_EncryptUpdate(srtm->blind_ctx, item->srt_blinded, &outl, + (const unsigned char *)token, sizeof(*token))) + return 0; + + if (!ossl_assert(outl == sizeof(*token))) + return 0; + + return 1; +} + +int ossl_quic_srtm_add(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num, + const QUIC_STATELESS_RESET_TOKEN *token) +{ + SRTM_ITEM *item = NULL, *head = NULL, *new_head, *r_item; + + if (srtm->alloc_failed) + return 0; + + /* (opaque, seq_num) duplicates not allowed */ + if ((item = srtm_find(srtm, opaque, seq_num, &head, NULL)) != NULL) + return 0; + + if ((item = OPENSSL_zalloc(sizeof(*item))) == NULL) + return 0; + + item->opaque = opaque; + item->seq_num = seq_num; + item->srt = *token; + if (!srtm_compute_blinded(srtm, item, &item->srt)) { + OPENSSL_free(item); + return 0; + } + + /* Add to forward mapping. */ + if (head == NULL) { + /* First item under this opaque */ + lh_SRTM_ITEM_insert(srtm->items_fwd, item); + if (!srtm_check_lh(srtm, srtm->items_fwd)) { + OPENSSL_free(item); + return 0; + } + } else { + sorted_insert_seq_num(head, item, &new_head); + if (new_head != head) { /* head changed, update in lhash */ + lh_SRTM_ITEM_insert(srtm->items_fwd, new_head); + if (!srtm_check_lh(srtm, srtm->items_fwd)) { + OPENSSL_free(item); + return 0; + } + } + } + + /* Add to reverse mapping. */ + r_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item); + if (r_item == NULL) { + /* First item under this blinded SRT */ + lh_SRTM_ITEM_insert(srtm->items_rev, item); + if (!srtm_check_lh(srtm, srtm->items_rev)) + /* + * Can't free the item now as we would have to undo the insertion + * into the forward mapping which would require an insert operation + * to restore the previous value. which might also fail. However, + * the item will be freed OK when we free the entire SRTM. + */ + return 0; + } else { + sorted_insert_srt(r_item, item, &new_head); + if (new_head != r_item) { /* head changed, update in lhash */ + lh_SRTM_ITEM_insert(srtm->items_rev, new_head); + if (!srtm_check_lh(srtm, srtm->items_rev)) + /* As above. */ + return 0; + } + } + + return 1; +} + +/* Remove item from reverse mapping. */ +static int srtm_remove_from_rev(QUIC_SRTM *srtm, SRTM_ITEM *item) +{ + SRTM_ITEM *rh_item; + + rh_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item); + assert(rh_item != NULL); + if (rh_item == item) { + /* + * Change lhash to point to item after this one, or remove the entry if + * this is the last one. + */ + if (item->next_by_srt_blinded != NULL) { + lh_SRTM_ITEM_insert(srtm->items_rev, item->next_by_srt_blinded); + if (!srtm_check_lh(srtm, srtm->items_rev)) + return 0; + } else { + lh_SRTM_ITEM_delete(srtm->items_rev, item); + } + } else { + /* Find our entry in the SRT list */ + for (; rh_item->next_by_srt_blinded != item; + rh_item = rh_item->next_by_srt_blinded); + rh_item->next_by_srt_blinded = item->next_by_srt_blinded; + } + + return 1; +} + +int ossl_quic_srtm_remove(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num) +{ + SRTM_ITEM *item, *prev = NULL; + + if (srtm->alloc_failed) + return 0; + + if ((item = srtm_find(srtm, opaque, seq_num, NULL, &prev)) == NULL) + /* No match */ + return 0; + + /* Remove from forward mapping. */ + if (prev == NULL) { + /* + * Change lhash to point to item after this one, or remove the entry if + * this is the last one. + */ + if (item->next_by_seq_num != NULL) { + lh_SRTM_ITEM_insert(srtm->items_fwd, item->next_by_seq_num); + if (!srtm_check_lh(srtm, srtm->items_fwd)) + return 0; + } else { + lh_SRTM_ITEM_delete(srtm->items_fwd, item); + } + } else { + prev->next_by_seq_num = item->next_by_seq_num; + } + + /* Remove from reverse mapping. */ + if (!srtm_remove_from_rev(srtm, item)) + return 0; + + OPENSSL_free(item); + return 1; +} + +int ossl_quic_srtm_cull(QUIC_SRTM *srtm, void *opaque) +{ + SRTM_ITEM key, *item = NULL, *inext, *ihead; + + key.opaque = opaque; + + if (srtm->alloc_failed) + return 0; + + if ((ihead = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key)) == NULL) + return 1; /* nothing removed is a success condition */ + + for (item = ihead; item != NULL; item = inext) { + inext = item->next_by_seq_num; + if (item != ihead) { + srtm_remove_from_rev(srtm, item); + OPENSSL_free(item); + } + } + + lh_SRTM_ITEM_delete(srtm->items_fwd, ihead); + srtm_remove_from_rev(srtm, ihead); + OPENSSL_free(ihead); + return 1; +} + +int ossl_quic_srtm_lookup(QUIC_SRTM *srtm, + const QUIC_STATELESS_RESET_TOKEN *token, + size_t idx, + void **opaque, uint64_t *seq_num) +{ + SRTM_ITEM key, *item; + + if (srtm->alloc_failed) + return 0; + + if (!srtm_compute_blinded(srtm, &key, token)) + return 0; + + item = lh_SRTM_ITEM_retrieve(srtm->items_rev, &key); + for (; idx > 0 && item != NULL; --idx, item = item->next_by_srt_blinded); + if (item == NULL) + return 0; + + if (opaque != NULL) + *opaque = item->opaque; + if (seq_num != NULL) + *seq_num = item->seq_num; + + return 1; +} + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + +static uint32_t token_next = 0x5eadbeef; +static size_t tokens_seen; + +struct check_args { + uint32_t token; + int mode; +}; + +static void check_mark(SRTM_ITEM *item, void *arg) +{ + struct check_args *arg_ = arg; + uint32_t token = arg_->token; + uint64_t prev_seq_num = 0; + void *prev_opaque = NULL; + int have_prev = 0; + + assert(item != NULL); + + while (item != NULL) { + if (have_prev) { + assert(!(item->opaque == prev_opaque && item->seq_num == prev_seq_num)); + if (!arg_->mode) + assert(item->opaque != prev_opaque || item->seq_num < prev_seq_num); + } + + ++tokens_seen; + item->debug_token = token; + prev_opaque = item->opaque; + prev_seq_num = item->seq_num; + have_prev = 1; + + if (arg_->mode) + item = item->next_by_srt_blinded; + else + item = item->next_by_seq_num; + } +} + +static void check_count(SRTM_ITEM *item, void *arg) +{ + struct check_args *arg_ = arg; + uint32_t token = arg_->token; + + assert(item != NULL); + + while (item != NULL) { + ++tokens_seen; + assert(item->debug_token == token); + + if (arg_->mode) + item = item->next_by_seq_num; + else + item = item->next_by_srt_blinded; + } +} + +#endif + +void ossl_quic_srtm_check(const QUIC_SRTM *srtm) +{ +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + struct check_args args = {0}; + size_t tokens_expected, tokens_expected_old; + + args.token = token_next; + ++token_next; + + assert(srtm != NULL); + assert(srtm->blind_ctx != NULL); + assert(srtm->items_fwd != NULL); + assert(srtm->items_rev != NULL); + + tokens_seen = 0; + lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_mark, &args); + + tokens_expected = tokens_seen; + tokens_seen = 0; + lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_count, &args); + + assert(tokens_seen == tokens_expected); + tokens_expected_old = tokens_expected; + + args.token = token_next; + ++token_next; + + args.mode = 1; + tokens_seen = 0; + lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_mark, &args); + + tokens_expected = tokens_seen; + tokens_seen = 0; + lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_count, &args); + + assert(tokens_seen == tokens_expected); + assert(tokens_seen == tokens_expected_old); +#endif +} |
