Home | History | Annotate | Line # | Download | only in quic
      1 /*
      2  * Copyright 2023-2026 The OpenSSL Project Authors. All Rights Reserved.
      3  *
      4  * Licensed under the Apache License 2.0 (the "License").  You may not use
      5  * this file except in compliance with the License.  You can obtain a copy
      6  * in the file LICENSE in the source distribution or at
      7  * https://www.openssl.org/source/license.html
      8  */
      9 
     10 #include "internal/quic_srtm.h"
     11 #include "internal/common.h"
     12 #include <openssl/lhash.h>
     13 #include <openssl/core_names.h>
     14 #include <openssl/rand.h>
     15 
     16 /*
     17  * QUIC Stateless Reset Token Manager
     18  * ==================================
     19  */
     20 typedef struct srtm_item_st SRTM_ITEM;
     21 
     22 #define BLINDED_SRT_LEN 16
     23 
     24 DEFINE_LHASH_OF_EX(SRTM_ITEM);
     25 
     26 /*
     27  * The SRTM is implemented using two LHASH instances, one matching opaque pointers to
     28  * an item structure, and another matching a SRT-derived value to an item
     29  * structure. Multiple items with different seq_num values under a given opaque,
     30  * and duplicate SRTs, are handled using sorted singly-linked lists.
     31  *
     32  * The O(n) insert and lookup performance is tolerated on the basis that the
     33  * total number of entries for a given opaque (total number of extant CIDs for a
     34  * connection) should be quite small, and the QUIC protocol allows us to place a
     35  * hard limit on this via the active_connection_id_limit TPARAM. Thus there is
     36  * no risk of a large number of SRTs needing to be registered under a given
     37  * opaque.
     38  *
     39  * It is expected one SRTM will exist per QUIC_PORT and track all SRTs across
     40  * all connections for that QUIC_PORT.
     41  */
     42 struct srtm_item_st {
     43     SRTM_ITEM *next_by_srt_blinded; /* SORT BY opaque  DESC */
     44     SRTM_ITEM *next_by_seq_num; /* SORT BY seq_num DESC */
     45     void *opaque; /* \__ unique identity for item */
     46     uint64_t seq_num; /* /                            */
     47     QUIC_STATELESS_RESET_TOKEN srt;
     48     unsigned char srt_blinded[BLINDED_SRT_LEN]; /* H(srt) */
     49 
     50 #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
     51     uint32_t debug_token;
     52 #endif
     53 };
     54 
     55 struct quic_srtm_st {
     56     /* Crypto context used to calculate blinded SRTs H(srt). */
     57     EVP_CIPHER_CTX *blind_ctx; /* kept with key */
     58 
     59     LHASH_OF(SRTM_ITEM) *items_fwd; /* (opaque)  -> SRTM_ITEM */
     60     LHASH_OF(SRTM_ITEM) *items_rev; /* (H(srt))  -> SRTM_ITEM */
     61 
     62     /*
     63      * Monotonically transitions to 1 in event of allocation failure. The only
     64      * valid operation on such an object is to free it.
     65      */
     66     unsigned int alloc_failed : 1;
     67 };
     68 
     69 static unsigned long items_fwd_hash(const SRTM_ITEM *item)
     70 {
     71     return (unsigned long)(uintptr_t)item->opaque;
     72 }
     73 
     74 static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
     75 {
     76     return a->opaque != b->opaque;
     77 }
     78 
     79 static unsigned long items_rev_hash(const SRTM_ITEM *item)
     80 {
     81     /*
     82      * srt_blinded has already been through a crypto-grade hash function, so we
     83      * can just use bits from that.
     84      */
     85     unsigned long l;
     86 
     87     memcpy(&l, item->srt_blinded, sizeof(l));
     88     return l;
     89 }
     90 
     91 static int items_rev_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
     92 {
     93     /*
     94      * We don't need to use CRYPTO_memcmp here as the relationship of
     95      * srt_blinded to srt is already cryptographically obfuscated.
     96      */
     97     return memcmp(a->srt_blinded, b->srt_blinded, sizeof(a->srt_blinded));
     98 }
     99 
    100 static int srtm_check_lh(QUIC_SRTM *srtm, LHASH_OF(SRTM_ITEM) *lh)
    101 {
    102     if (lh_SRTM_ITEM_error(lh)) {
    103         srtm->alloc_failed = 1;
    104         return 0;
    105     }
    106 
    107     return 1;
    108 }
    109 
    110 QUIC_SRTM *ossl_quic_srtm_new(OSSL_LIB_CTX *libctx, const char *propq)
    111 {
    112     QUIC_SRTM *srtm = NULL;
    113     unsigned char key[16];
    114     EVP_CIPHER *ecb = NULL;
    115 
    116     if (RAND_priv_bytes_ex(libctx, key, sizeof(key), sizeof(key) * 8) != 1)
    117         goto err;
    118 
    119     if ((srtm = OPENSSL_zalloc(sizeof(*srtm))) == NULL)
    120         return NULL;
    121 
    122     /* Use AES-128-ECB as a permutation over 128-bit SRTs. */
    123     if ((ecb = EVP_CIPHER_fetch(libctx, "AES-128-ECB", propq)) == NULL)
    124         goto err;
    125 
    126     if ((srtm->blind_ctx = EVP_CIPHER_CTX_new()) == NULL)
    127         goto err;
    128 
    129     if (!EVP_EncryptInit_ex2(srtm->blind_ctx, ecb, key, NULL, NULL))
    130         goto err;
    131 
    132     EVP_CIPHER_free(ecb);
    133     ecb = NULL;
    134 
    135     /* Create mappings. */
    136     if ((srtm->items_fwd = lh_SRTM_ITEM_new(items_fwd_hash, items_fwd_cmp)) == NULL
    137         || (srtm->items_rev = lh_SRTM_ITEM_new(items_rev_hash, items_rev_cmp)) == NULL)
    138         goto err;
    139 
    140     return srtm;
    141 
    142 err:
    143     /*
    144      * No cleansing of key needed as blinding exists only for side channel
    145      * mitigation.
    146      */
    147     ossl_quic_srtm_free(srtm);
    148     EVP_CIPHER_free(ecb);
    149     return NULL;
    150 }
    151 
    152 static void srtm_free_each(SRTM_ITEM *ihead)
    153 {
    154     SRTM_ITEM *inext, *item = ihead;
    155 
    156     for (item = item->next_by_seq_num; item != NULL; item = inext) {
    157         inext = item->next_by_seq_num;
    158         OPENSSL_free(item);
    159     }
    160 
    161     OPENSSL_free(ihead);
    162 }
    163 
    164 void ossl_quic_srtm_free(QUIC_SRTM *srtm)
    165 {
    166     if (srtm == NULL)
    167         return;
    168 
    169     lh_SRTM_ITEM_free(srtm->items_rev);
    170     if (srtm->items_fwd != NULL) {
    171         /*
    172          * We don't need to call lh_SRTM_ITEM_set_down_load(..., 0)
    173          * here because srtm_free_each() callback for _doall() does
    174          * not call to lh_SRTIM_ITEM_delete().
    175          */
    176         lh_SRTM_ITEM_doall(srtm->items_fwd, srtm_free_each);
    177         lh_SRTM_ITEM_free(srtm->items_fwd);
    178     }
    179 
    180     EVP_CIPHER_CTX_free(srtm->blind_ctx);
    181     OPENSSL_free(srtm);
    182 }
    183 
    184 /*
    185  * Find a SRTM_ITEM by (opaque, seq_num). Returns NULL if no match.
    186  * If head is non-NULL, writes the head of the relevant opaque list to *head if
    187  * there is one.
    188  * If prev is non-NULL, writes the previous node to *prev or NULL if it is
    189  * the first item.
    190  */
    191 static SRTM_ITEM *srtm_find(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num,
    192     SRTM_ITEM **head_p, SRTM_ITEM **prev_p)
    193 {
    194     SRTM_ITEM key, *item = NULL, *prev = NULL;
    195 
    196     key.opaque = opaque;
    197 
    198     item = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key);
    199     if (head_p != NULL)
    200         *head_p = item;
    201 
    202     for (; item != NULL; prev = item, item = item->next_by_seq_num)
    203         if (item->seq_num == seq_num) {
    204             break;
    205         } else if (item->seq_num < seq_num) {
    206             /*
    207              * List is sorted in descending order so there can't be any match
    208              * after this.
    209              */
    210             item = NULL;
    211             break;
    212         }
    213 
    214     if (prev_p != NULL)
    215         *prev_p = prev;
    216 
    217     return item;
    218 }
    219 
    220 /*
    221  * Inserts a SRTM_ITEM into the singly-linked by-sequence-number linked list.
    222  * The new head pointer is written to *new_head (which may or may not be
    223  * unchanged).
    224  */
    225 static void sorted_insert_seq_num(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head)
    226 {
    227     uint64_t seq_num = item->seq_num;
    228     SRTM_ITEM *cur = head, **fixup = new_head;
    229 
    230     *new_head = head;
    231 
    232     while (cur != NULL && cur->seq_num > seq_num) {
    233         fixup = &cur->next_by_seq_num;
    234         cur = cur->next_by_seq_num;
    235     }
    236 
    237     item->next_by_seq_num = *fixup;
    238     *fixup = item;
    239 }
    240 
    241 /*
    242  * Inserts a SRTM_ITEM into the singly-linked by-SRT list.
    243  * The new head pointer is written to *new_head (which may or may not be
    244  * unchanged).
    245  */
    246 static void sorted_insert_srt(SRTM_ITEM *head, SRTM_ITEM *item, SRTM_ITEM **new_head)
    247 {
    248     uintptr_t opaque = (uintptr_t)item->opaque;
    249     SRTM_ITEM *cur = head, **fixup = new_head;
    250 
    251     *new_head = head;
    252 
    253     while (cur != NULL && (uintptr_t)cur->opaque > opaque) {
    254         fixup = &cur->next_by_srt_blinded;
    255         cur = cur->next_by_srt_blinded;
    256     }
    257 
    258     item->next_by_srt_blinded = *fixup;
    259     *fixup = item;
    260 }
    261 
    262 /*
    263  * Computes the blinded SRT value used for internal lookup for side channel
    264  * mitigation purposes. We compute this once as a cached value when an SRTM_ITEM
    265  * is formed.
    266  */
    267 static int srtm_compute_blinded(QUIC_SRTM *srtm, SRTM_ITEM *item,
    268     const QUIC_STATELESS_RESET_TOKEN *token)
    269 {
    270     int outl = 0;
    271 
    272     /*
    273      * We use AES-128-ECB as a permutation using a random key to facilitate
    274      * blinding for side-channel purposes. Encrypt the token as a single AES
    275      * block.
    276      */
    277     if (!EVP_EncryptUpdate(srtm->blind_ctx, item->srt_blinded, &outl,
    278             (const unsigned char *)token, sizeof(*token)))
    279         return 0;
    280 
    281     if (!ossl_assert(outl == sizeof(*token)))
    282         return 0;
    283 
    284     return 1;
    285 }
    286 
    287 int ossl_quic_srtm_add(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num,
    288     const QUIC_STATELESS_RESET_TOKEN *token)
    289 {
    290     SRTM_ITEM *item = NULL, *head = NULL, *new_head, *r_item;
    291 
    292     if (srtm->alloc_failed)
    293         return 0;
    294 
    295     /* (opaque, seq_num) duplicates not allowed */
    296     if ((item = srtm_find(srtm, opaque, seq_num, &head, NULL)) != NULL)
    297         return 0;
    298 
    299     if ((item = OPENSSL_zalloc(sizeof(*item))) == NULL)
    300         return 0;
    301 
    302     item->opaque = opaque;
    303     item->seq_num = seq_num;
    304     item->srt = *token;
    305     if (!srtm_compute_blinded(srtm, item, &item->srt)) {
    306         OPENSSL_free(item);
    307         return 0;
    308     }
    309 
    310     /* Add to forward mapping. */
    311     if (head == NULL) {
    312         /* First item under this opaque */
    313         lh_SRTM_ITEM_insert(srtm->items_fwd, item);
    314         if (!srtm_check_lh(srtm, srtm->items_fwd)) {
    315             OPENSSL_free(item);
    316             return 0;
    317         }
    318     } else {
    319         sorted_insert_seq_num(head, item, &new_head);
    320         if (new_head != head) { /* head changed, update in lhash */
    321             lh_SRTM_ITEM_insert(srtm->items_fwd, new_head);
    322             if (!srtm_check_lh(srtm, srtm->items_fwd)) {
    323                 OPENSSL_free(item);
    324                 return 0;
    325             }
    326         }
    327     }
    328 
    329     /* Add to reverse mapping. */
    330     r_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
    331     if (r_item == NULL) {
    332         /* First item under this blinded SRT */
    333         lh_SRTM_ITEM_insert(srtm->items_rev, item);
    334         if (!srtm_check_lh(srtm, srtm->items_rev))
    335             /*
    336              * Can't free the item now as we would have to undo the insertion
    337              * into the forward mapping which would require an insert operation
    338              * to restore the previous value. which might also fail. However,
    339              * the item will be freed OK when we free the entire SRTM.
    340              */
    341             return 0;
    342     } else {
    343         sorted_insert_srt(r_item, item, &new_head);
    344         if (new_head != r_item) { /* head changed, update in lhash */
    345             lh_SRTM_ITEM_insert(srtm->items_rev, new_head);
    346             if (!srtm_check_lh(srtm, srtm->items_rev))
    347                 /* As above. */
    348                 return 0;
    349         }
    350     }
    351 
    352     return 1;
    353 }
    354 
    355 /* Remove item from reverse mapping. */
    356 static int srtm_remove_from_rev(QUIC_SRTM *srtm, SRTM_ITEM *item)
    357 {
    358     SRTM_ITEM *rh_item;
    359 
    360     rh_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
    361     assert(rh_item != NULL);
    362     if (rh_item == item) {
    363         /*
    364          * Change lhash to point to item after this one, or remove the entry if
    365          * this is the last one.
    366          */
    367         if (item->next_by_srt_blinded != NULL) {
    368             lh_SRTM_ITEM_insert(srtm->items_rev, item->next_by_srt_blinded);
    369             if (!srtm_check_lh(srtm, srtm->items_rev))
    370                 return 0;
    371         } else {
    372             lh_SRTM_ITEM_delete(srtm->items_rev, item);
    373         }
    374     } else {
    375         /* Find our entry in the SRT list */
    376         for (; rh_item->next_by_srt_blinded != item;
    377             rh_item = rh_item->next_by_srt_blinded)
    378             ;
    379         rh_item->next_by_srt_blinded = item->next_by_srt_blinded;
    380     }
    381 
    382     return 1;
    383 }
    384 
    385 int ossl_quic_srtm_remove(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num)
    386 {
    387     SRTM_ITEM *item, *prev = NULL;
    388 
    389     if (srtm->alloc_failed)
    390         return 0;
    391 
    392     if ((item = srtm_find(srtm, opaque, seq_num, NULL, &prev)) == NULL)
    393         /* No match */
    394         return 0;
    395 
    396     /* Remove from forward mapping. */
    397     if (prev == NULL) {
    398         /*
    399          * Change lhash to point to item after this one, or remove the entry if
    400          * this is the last one.
    401          */
    402         if (item->next_by_seq_num != NULL) {
    403             lh_SRTM_ITEM_insert(srtm->items_fwd, item->next_by_seq_num);
    404             if (!srtm_check_lh(srtm, srtm->items_fwd))
    405                 return 0;
    406         } else {
    407             lh_SRTM_ITEM_delete(srtm->items_fwd, item);
    408         }
    409     } else {
    410         prev->next_by_seq_num = item->next_by_seq_num;
    411     }
    412 
    413     /* Remove from reverse mapping. */
    414     if (!srtm_remove_from_rev(srtm, item))
    415         return 0;
    416 
    417     OPENSSL_free(item);
    418     return 1;
    419 }
    420 
    421 int ossl_quic_srtm_cull(QUIC_SRTM *srtm, void *opaque)
    422 {
    423     SRTM_ITEM key, *item = NULL, *inext, *ihead;
    424 
    425     key.opaque = opaque;
    426 
    427     if (srtm->alloc_failed)
    428         return 0;
    429 
    430     if ((ihead = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key)) == NULL)
    431         return 1; /* nothing removed is a success condition */
    432 
    433     for (item = ihead; item != NULL; item = inext) {
    434         inext = item->next_by_seq_num;
    435         if (item != ihead) {
    436             srtm_remove_from_rev(srtm, item);
    437             OPENSSL_free(item);
    438         }
    439     }
    440 
    441     lh_SRTM_ITEM_delete(srtm->items_fwd, ihead);
    442     srtm_remove_from_rev(srtm, ihead);
    443     OPENSSL_free(ihead);
    444     return 1;
    445 }
    446 
    447 int ossl_quic_srtm_lookup(QUIC_SRTM *srtm,
    448     const QUIC_STATELESS_RESET_TOKEN *token,
    449     size_t idx,
    450     void **opaque, uint64_t *seq_num)
    451 {
    452     SRTM_ITEM key, *item;
    453 
    454     if (srtm->alloc_failed)
    455         return 0;
    456 
    457     if (!srtm_compute_blinded(srtm, &key, token))
    458         return 0;
    459 
    460     item = lh_SRTM_ITEM_retrieve(srtm->items_rev, &key);
    461     for (; idx > 0 && item != NULL; --idx, item = item->next_by_srt_blinded)
    462         ;
    463     if (item == NULL)
    464         return 0;
    465 
    466     if (opaque != NULL)
    467         *opaque = item->opaque;
    468     if (seq_num != NULL)
    469         *seq_num = item->seq_num;
    470 
    471     return 1;
    472 }
    473 
    474 #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
    475 
    476 static uint32_t token_next = 0x5eadbeef;
    477 static size_t tokens_seen;
    478 
    479 struct check_args {
    480     uint32_t token;
    481     int mode;
    482 };
    483 
    484 static void check_mark(SRTM_ITEM *item, void *arg)
    485 {
    486     struct check_args *arg_ = arg;
    487     uint32_t token = arg_->token;
    488     uint64_t prev_seq_num = 0;
    489     void *prev_opaque = NULL;
    490     int have_prev = 0;
    491 
    492     assert(item != NULL);
    493 
    494     while (item != NULL) {
    495         if (have_prev) {
    496             assert(!(item->opaque == prev_opaque && item->seq_num == prev_seq_num));
    497             if (!arg_->mode)
    498                 assert(item->opaque != prev_opaque || item->seq_num < prev_seq_num);
    499         }
    500 
    501         ++tokens_seen;
    502         item->debug_token = token;
    503         prev_opaque = item->opaque;
    504         prev_seq_num = item->seq_num;
    505         have_prev = 1;
    506 
    507         if (arg_->mode)
    508             item = item->next_by_srt_blinded;
    509         else
    510             item = item->next_by_seq_num;
    511     }
    512 }
    513 
    514 static void check_count(SRTM_ITEM *item, void *arg)
    515 {
    516     struct check_args *arg_ = arg;
    517     uint32_t token = arg_->token;
    518 
    519     assert(item != NULL);
    520 
    521     while (item != NULL) {
    522         ++tokens_seen;
    523         assert(item->debug_token == token);
    524 
    525         if (arg_->mode)
    526             item = item->next_by_seq_num;
    527         else
    528             item = item->next_by_srt_blinded;
    529     }
    530 }
    531 
    532 #endif
    533 
    534 void ossl_quic_srtm_check(const QUIC_SRTM *srtm)
    535 {
    536 #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
    537     struct check_args args = { 0 };
    538     size_t tokens_expected, tokens_expected_old;
    539 
    540     args.token = token_next;
    541     ++token_next;
    542 
    543     assert(srtm != NULL);
    544     assert(srtm->blind_ctx != NULL);
    545     assert(srtm->items_fwd != NULL);
    546     assert(srtm->items_rev != NULL);
    547 
    548     tokens_seen = 0;
    549     lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_mark, &args);
    550 
    551     tokens_expected = tokens_seen;
    552     tokens_seen = 0;
    553     lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_count, &args);
    554 
    555     assert(tokens_seen == tokens_expected);
    556     tokens_expected_old = tokens_expected;
    557 
    558     args.token = token_next;
    559     ++token_next;
    560 
    561     args.mode = 1;
    562     tokens_seen = 0;
    563     lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_mark, &args);
    564 
    565     tokens_expected = tokens_seen;
    566     tokens_seen = 0;
    567     lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_count, &args);
    568 
    569     assert(tokens_seen == tokens_expected);
    570     assert(tokens_seen == tokens_expected_old);
    571 #endif
    572 }
    573