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
items_fwd_hash(const SRTM_ITEM * item)69 static unsigned long items_fwd_hash(const SRTM_ITEM *item)
70 {
71 return (unsigned long)(uintptr_t)item->opaque;
72 }
73
items_fwd_cmp(const SRTM_ITEM * a,const SRTM_ITEM * b)74 static int items_fwd_cmp(const SRTM_ITEM *a, const SRTM_ITEM *b)
75 {
76 return a->opaque != b->opaque;
77 }
78
items_rev_hash(const SRTM_ITEM * item)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
items_rev_cmp(const SRTM_ITEM * a,const SRTM_ITEM * b)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
srtm_check_lh(QUIC_SRTM * srtm,LHASH_OF (SRTM_ITEM)* lh)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
ossl_quic_srtm_new(OSSL_LIB_CTX * libctx,const char * propq)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
srtm_free_each(SRTM_ITEM * ihead)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
ossl_quic_srtm_free(QUIC_SRTM * srtm)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 */
srtm_find(QUIC_SRTM * srtm,void * opaque,uint64_t seq_num,SRTM_ITEM ** head_p,SRTM_ITEM ** prev_p)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 */
sorted_insert_seq_num(SRTM_ITEM * head,SRTM_ITEM * item,SRTM_ITEM ** new_head)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 */
sorted_insert_srt(SRTM_ITEM * head,SRTM_ITEM * item,SRTM_ITEM ** new_head)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 */
srtm_compute_blinded(QUIC_SRTM * srtm,SRTM_ITEM * item,const QUIC_STATELESS_RESET_TOKEN * token)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
ossl_quic_srtm_add(QUIC_SRTM * srtm,void * opaque,uint64_t seq_num,const QUIC_STATELESS_RESET_TOKEN * token)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. */
srtm_remove_from_rev(QUIC_SRTM * srtm,SRTM_ITEM * item)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
ossl_quic_srtm_remove(QUIC_SRTM * srtm,void * opaque,uint64_t seq_num)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
ossl_quic_srtm_cull(QUIC_SRTM * srtm,void * opaque)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
ossl_quic_srtm_lookup(QUIC_SRTM * srtm,const QUIC_STATELESS_RESET_TOKEN * token,size_t idx,void ** opaque,uint64_t * seq_num)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
check_mark(SRTM_ITEM * item,void * arg)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
check_count(SRTM_ITEM * item,void * arg)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
ossl_quic_srtm_check(const QUIC_SRTM * srtm)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