Coverage Report

Created: 2026-04-09 06:50

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