Coverage Report

Created: 2026-04-12 07:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/openssl/ssl/quic/quic_srtm.c
Line
Count
Source
1
/*
2
 * Copyright 2023-2024 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
0
{
71
0
    return (unsigned long)(uintptr_t)item->opaque;
72
0
}
73
74
static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
75
0
{
76
0
    return a->opaque != b->opaque;
77
0
}
78
79
static unsigned long items_rev_hash(const SRTM_ITEM *item)
80
0
{
81
    /*
82
     * srt_blinded has already been through a crypto-grade hash function, so we
83
     * can just use bits from that.
84
     */
85
0
    unsigned long l;
86
87
0
    memcpy(&l, item->srt_blinded, sizeof(l));
88
0
    return l;
89
0
}
90
91
static int items_rev_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
92
0
{
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
0
    return memcmp(a->srt_blinded, b->srt_blinded, sizeof(a->srt_blinded));
98
0
}
99
100
static int srtm_check_lh(QUIC_SRTM *srtm, LHASH_OF(SRTM_ITEM) *lh)
101
0
{
102
0
    if (lh_SRTM_ITEM_error(lh)) {
103
0
        srtm->alloc_failed = 1;
104
0
        return 0;
105
0
    }
106
107
0
    return 1;
108
0
}
109
110
QUIC_SRTM *ossl_quic_srtm_new(OSSL_LIB_CTX *libctx, const char *propq)
111
0
{
112
0
    QUIC_SRTM *srtm = NULL;
113
0
    unsigned char key[16];
114
0
    EVP_CIPHER *ecb = NULL;
115
116
0
    if (RAND_priv_bytes_ex(libctx, key, sizeof(key), sizeof(key) * 8) != 1)
117
0
        goto err;
118
119
0
    if ((srtm = OPENSSL_zalloc(sizeof(*srtm))) == NULL)
120
0
        return NULL;
121
122
    /* Use AES-128-ECB as a permutation over 128-bit SRTs. */
123
0
    if ((ecb = EVP_CIPHER_fetch(libctx, "AES-128-ECB", propq)) == NULL)
124
0
        goto err;
125
126
0
    if ((srtm->blind_ctx = EVP_CIPHER_CTX_new()) == NULL)
127
0
        goto err;
128
129
0
    if (!EVP_EncryptInit_ex2(srtm->blind_ctx, ecb, key, NULL, NULL))
130
0
        goto err;
131
132
0
    EVP_CIPHER_free(ecb);
133
0
    ecb = NULL;
134
135
    /* Create mappings. */
136
0
    if ((srtm->items_fwd = lh_SRTM_ITEM_new(items_fwd_hash, items_fwd_cmp)) == NULL
137
0
        || (srtm->items_rev = lh_SRTM_ITEM_new(items_rev_hash, items_rev_cmp)) == NULL)
138
0
        goto err;
139
140
0
    return srtm;
141
142
0
err:
143
    /*
144
     * No cleansing of key needed as blinding exists only for side channel
145
     * mitigation.
146
     */
147
0
    ossl_quic_srtm_free(srtm);
148
0
    EVP_CIPHER_free(ecb);
149
0
    return NULL;
150
0
}
151
152
static void srtm_free_each(SRTM_ITEM *ihead)
153
0
{
154
0
    SRTM_ITEM *inext, *item = ihead;
155
156
0
    for (item = item->next_by_seq_num; item != NULL; item = inext) {
157
0
        inext = item->next_by_seq_num;
158
0
        OPENSSL_free(item);
159
0
    }
160
161
0
    OPENSSL_free(ihead);
162
0
}
163
164
void ossl_quic_srtm_free(QUIC_SRTM *srtm)
165
0
{
166
0
    if (srtm == NULL)
167
0
        return;
168
169
0
    lh_SRTM_ITEM_free(srtm->items_rev);
170
0
    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
0
        lh_SRTM_ITEM_doall(srtm->items_fwd, srtm_free_each);
177
0
        lh_SRTM_ITEM_free(srtm->items_fwd);
178
0
    }
179
180
0
    EVP_CIPHER_CTX_free(srtm->blind_ctx);
181
0
    OPENSSL_free(srtm);
182
0
}
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
0
{
194
0
    SRTM_ITEM key, *item = NULL, *prev = NULL;
195
196
0
    key.opaque = opaque;
197
198
0
    item = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key);
199
0
    if (head_p != NULL)
200
0
        *head_p = item;
201
202
0
    for (; item != NULL; prev = item, item = item->next_by_seq_num)
203
0
        if (item->seq_num == seq_num) {
204
0
            break;
205
0
        } 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
0
            item = NULL;
211
0
            break;
212
0
        }
213
214
0
    if (prev_p != NULL)
215
0
        *prev_p = prev;
216
217
0
    return item;
218
0
}
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
0
{
227
0
    uint64_t seq_num = item->seq_num;
228
0
    SRTM_ITEM *cur = head, **fixup = new_head;
229
230
0
    *new_head = head;
231
232
0
    while (cur != NULL && cur->seq_num > seq_num) {
233
0
        fixup = &cur->next_by_seq_num;
234
0
        cur = cur->next_by_seq_num;
235
0
    }
236
237
0
    item->next_by_seq_num = *fixup;
238
0
    *fixup = item;
239
0
}
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
0
{
248
0
    uintptr_t opaque = (uintptr_t)item->opaque;
249
0
    SRTM_ITEM *cur = head, **fixup = new_head;
250
251
0
    *new_head = head;
252
253
0
    while (cur != NULL && (uintptr_t)cur->opaque > opaque) {
254
0
        fixup = &cur->next_by_srt_blinded;
255
0
        cur = cur->next_by_srt_blinded;
256
0
    }
257
258
0
    item->next_by_srt_blinded = *fixup;
259
0
    *fixup = item;
260
0
}
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
0
{
270
0
    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
0
    if (!EVP_EncryptUpdate(srtm->blind_ctx, item->srt_blinded, &outl,
278
0
            (const unsigned char *)token, sizeof(*token)))
279
0
        return 0;
280
281
0
    if (!ossl_assert(outl == sizeof(*token)))
282
0
        return 0;
283
284
0
    return 1;
285
0
}
286
287
int ossl_quic_srtm_add(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num,
288
    const QUIC_STATELESS_RESET_TOKEN *token)
289
0
{
290
0
    SRTM_ITEM *item = NULL, *head = NULL, *new_head, *r_item;
291
292
0
    if (srtm->alloc_failed)
293
0
        return 0;
294
295
    /* (opaque, seq_num) duplicates not allowed */
296
0
    if ((item = srtm_find(srtm, opaque, seq_num, &head, NULL)) != NULL)
297
0
        return 0;
298
299
0
    if ((item = OPENSSL_zalloc(sizeof(*item))) == NULL)
300
0
        return 0;
301
302
0
    item->opaque = opaque;
303
0
    item->seq_num = seq_num;
304
0
    item->srt = *token;
305
0
    if (!srtm_compute_blinded(srtm, item, &item->srt)) {
306
0
        OPENSSL_free(item);
307
0
        return 0;
308
0
    }
309
310
    /* Add to forward mapping. */
311
0
    if (head == NULL) {
312
        /* First item under this opaque */
313
0
        lh_SRTM_ITEM_insert(srtm->items_fwd, item);
314
0
        if (!srtm_check_lh(srtm, srtm->items_fwd)) {
315
0
            OPENSSL_free(item);
316
0
            return 0;
317
0
        }
318
0
    } else {
319
0
        sorted_insert_seq_num(head, item, &new_head);
320
0
        if (new_head != head) { /* head changed, update in lhash */
321
0
            lh_SRTM_ITEM_insert(srtm->items_fwd, new_head);
322
0
            if (!srtm_check_lh(srtm, srtm->items_fwd)) {
323
0
                OPENSSL_free(item);
324
0
                return 0;
325
0
            }
326
0
        }
327
0
    }
328
329
    /* Add to reverse mapping. */
330
0
    r_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
331
0
    if (r_item == NULL) {
332
        /* First item under this blinded SRT */
333
0
        lh_SRTM_ITEM_insert(srtm->items_rev, item);
334
0
        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
0
            return 0;
342
0
    } else {
343
0
        sorted_insert_srt(r_item, item, &new_head);
344
0
        if (new_head != r_item) { /* head changed, update in lhash */
345
0
            lh_SRTM_ITEM_insert(srtm->items_rev, new_head);
346
0
            if (!srtm_check_lh(srtm, srtm->items_rev))
347
                /* As above. */
348
0
                return 0;
349
0
        }
350
0
    }
351
352
0
    return 1;
353
0
}
354
355
/* Remove item from reverse mapping. */
356
static int srtm_remove_from_rev(QUIC_SRTM *srtm, SRTM_ITEM *item)
357
0
{
358
0
    SRTM_ITEM *rh_item;
359
360
0
    rh_item = lh_SRTM_ITEM_retrieve(srtm->items_rev, item);
361
0
    assert(rh_item != NULL);
362
0
    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
0
        if (item->next_by_srt_blinded != NULL) {
368
0
            lh_SRTM_ITEM_insert(srtm->items_rev, item->next_by_srt_blinded);
369
0
            if (!srtm_check_lh(srtm, srtm->items_rev))
370
0
                return 0;
371
0
        } else {
372
0
            lh_SRTM_ITEM_delete(srtm->items_rev, item);
373
0
        }
374
0
    } else {
375
        /* Find our entry in the SRT list */
376
0
        for (; rh_item->next_by_srt_blinded != item;
377
0
            rh_item = rh_item->next_by_srt_blinded)
378
0
            ;
379
0
        rh_item->next_by_srt_blinded = item->next_by_srt_blinded;
380
0
    }
381
382
0
    return 1;
383
0
}
384
385
int ossl_quic_srtm_remove(QUIC_SRTM *srtm, void *opaque, uint64_t seq_num)
386
0
{
387
0
    SRTM_ITEM *item, *prev = NULL;
388
389
0
    if (srtm->alloc_failed)
390
0
        return 0;
391
392
0
    if ((item = srtm_find(srtm, opaque, seq_num, NULL, &prev)) == NULL)
393
        /* No match */
394
0
        return 0;
395
396
    /* Remove from forward mapping. */
397
0
    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
0
        if (item->next_by_seq_num != NULL) {
403
0
            lh_SRTM_ITEM_insert(srtm->items_fwd, item->next_by_seq_num);
404
0
            if (!srtm_check_lh(srtm, srtm->items_fwd))
405
0
                return 0;
406
0
        } else {
407
0
            lh_SRTM_ITEM_delete(srtm->items_fwd, item);
408
0
        }
409
0
    } else {
410
0
        prev->next_by_seq_num = item->next_by_seq_num;
411
0
    }
412
413
    /* Remove from reverse mapping. */
414
0
    if (!srtm_remove_from_rev(srtm, item))
415
0
        return 0;
416
417
0
    OPENSSL_free(item);
418
0
    return 1;
419
0
}
420
421
int ossl_quic_srtm_cull(QUIC_SRTM *srtm, void *opaque)
422
0
{
423
0
    SRTM_ITEM key, *item = NULL, *inext, *ihead;
424
425
0
    key.opaque = opaque;
426
427
0
    if (srtm->alloc_failed)
428
0
        return 0;
429
430
0
    if ((ihead = lh_SRTM_ITEM_retrieve(srtm->items_fwd, &key)) == NULL)
431
0
        return 1; /* nothing removed is a success condition */
432
433
0
    for (item = ihead; item != NULL; item = inext) {
434
0
        inext = item->next_by_seq_num;
435
0
        if (item != ihead) {
436
0
            srtm_remove_from_rev(srtm, item);
437
0
            OPENSSL_free(item);
438
0
        }
439
0
    }
440
441
0
    lh_SRTM_ITEM_delete(srtm->items_fwd, ihead);
442
0
    srtm_remove_from_rev(srtm, ihead);
443
0
    OPENSSL_free(ihead);
444
0
    return 1;
445
0
}
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
0
{
452
0
    SRTM_ITEM key, *item;
453
454
0
    if (srtm->alloc_failed)
455
0
        return 0;
456
457
0
    if (!srtm_compute_blinded(srtm, &key, token))
458
0
        return 0;
459
460
0
    item = lh_SRTM_ITEM_retrieve(srtm->items_rev, &key);
461
0
    for (; idx > 0 && item != NULL; --idx, item = item->next_by_srt_blinded)
462
0
        ;
463
0
    if (item == NULL)
464
0
        return 0;
465
466
0
    if (opaque != NULL)
467
0
        *opaque = item->opaque;
468
0
    if (seq_num != NULL)
469
0
        *seq_num = item->seq_num;
470
471
0
    return 1;
472
0
}
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
0
{
486
0
    struct check_args *arg_ = arg;
487
0
    uint32_t token = arg_->token;
488
0
    uint64_t prev_seq_num = 0;
489
0
    void *prev_opaque = NULL;
490
0
    int have_prev = 0;
491
492
0
    assert(item != NULL);
493
494
0
    while (item != NULL) {
495
0
        if (have_prev) {
496
0
            assert(!(item->opaque == prev_opaque && item->seq_num == prev_seq_num));
497
0
            if (!arg_->mode)
498
0
                assert(item->opaque != prev_opaque || item->seq_num < prev_seq_num);
499
0
        }
500
501
0
        ++tokens_seen;
502
0
        item->debug_token = token;
503
0
        prev_opaque = item->opaque;
504
0
        prev_seq_num = item->seq_num;
505
0
        have_prev = 1;
506
507
0
        if (arg_->mode)
508
0
            item = item->next_by_srt_blinded;
509
0
        else
510
0
            item = item->next_by_seq_num;
511
0
    }
512
0
}
513
514
static void check_count(SRTM_ITEM *item, void *arg)
515
0
{
516
0
    struct check_args *arg_ = arg;
517
0
    uint32_t token = arg_->token;
518
519
0
    assert(item != NULL);
520
521
0
    while (item != NULL) {
522
0
        ++tokens_seen;
523
0
        assert(item->debug_token == token);
524
525
0
        if (arg_->mode)
526
0
            item = item->next_by_seq_num;
527
0
        else
528
0
            item = item->next_by_srt_blinded;
529
0
    }
530
0
}
531
532
#endif
533
534
void ossl_quic_srtm_check(const QUIC_SRTM *srtm)
535
0
{
536
0
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
537
0
    struct check_args args = { 0 };
538
0
    size_t tokens_expected, tokens_expected_old;
539
540
0
    args.token = token_next;
541
0
    ++token_next;
542
543
0
    assert(srtm != NULL);
544
0
    assert(srtm->blind_ctx != NULL);
545
0
    assert(srtm->items_fwd != NULL);
546
0
    assert(srtm->items_rev != NULL);
547
548
0
    tokens_seen = 0;
549
0
    lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_mark, &args);
550
551
0
    tokens_expected = tokens_seen;
552
0
    tokens_seen = 0;
553
0
    lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_count, &args);
554
555
0
    assert(tokens_seen == tokens_expected);
556
0
    tokens_expected_old = tokens_expected;
557
558
0
    args.token = token_next;
559
0
    ++token_next;
560
561
0
    args.mode = 1;
562
0
    tokens_seen = 0;
563
0
    lh_SRTM_ITEM_doall_arg(srtm->items_rev, check_mark, &args);
564
565
0
    tokens_expected = tokens_seen;
566
0
    tokens_seen = 0;
567
0
    lh_SRTM_ITEM_doall_arg(srtm->items_fwd, check_count, &args);
568
569
0
    assert(tokens_seen == tokens_expected);
570
    assert(tokens_seen == tokens_expected_old);
571
0
#endif
572
0
}