aboutsummaryrefslogtreecommitdiffstats
path: root/test/tls_groups_list_test.c
blob: 977039363475f2b58b651ecb49db4b6eef222083 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
/*
 * Copyright 2026 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
 */

/*
 * Tests for the TLS supported-groups list parser (tls1_set_groups_list()),
 * driven through the public SSL_CTX_set1_groups_list() entry point.
 *
 * The parser maintains three flat arrays and their bookkeeping in SSL_CTX:
 *   ctx->ext.supportedgroups[0..supportedgroups_len)  - groups, in order
 *   ctx->ext.tuples[0..tuples_len)                    - group count per tuple
 *   ctx->ext.keyshares[0..keyshares_len)              - keyshare group IDs
 * with the governing invariant that the per-tuple counts sum to the group
 * count: sum(tuples) == supportedgroups_len.  Those fields are not visible
 * through the public API, so we include ssl_local.h and check them directly.
 *
 * Several of the cases below are regressions for GitHub #31315, where the
 * remove-group path could leave tuples/keyshares out of step with the group
 * array (manifesting as an out-of-bounds read under a sanitizer).
 */

#include <openssl/ssl.h>
#include "internal/nelem.h"
#include "internal/tlsgroups.h"
#include "../ssl/ssl_local.h"
#include "testutil.h"

#define MAX_GROUPS 8
#define MAX_TUPLES 8
#define MAX_KS 8

/*
 * Sentinel used in ctx->ext.keyshares to mean "a single keyshare from the
 * first supported group" (set when no '*' prefix appears anywhere).
 */
#define KS_FIRST 0

typedef struct {
    const char *desc;
    const char *list; /* input passed to set1_groups_list */
    uint16_t groups[MAX_GROUPS]; /* expected groups, in order */
    size_t ngroups;
    size_t tuples[MAX_TUPLES]; /* expected per-tuple group counts */
    size_t ntuples;
    uint16_t keyshares[MAX_KS]; /* expected keyshares (KS_FIRST == 0) */
    size_t nkeyshares;
} TESTCASE;

static const TESTCASE cases[] = {
    /* --- Well-formed baselines --------------------------------------- */
    {
        "single tuple, two groups, implicit keyshare",
        "X25519:prime256v1",
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp256r1 },
        2,
        { 2 },
        1,
        { KS_FIRST },
        1,
    },
    {
        "two tuples, implicit keyshare",
        "X25519/prime256v1",
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp256r1 },
        2,
        { 1, 1 },
        2,
        { KS_FIRST },
        1,
    },
    {
        "explicit keyshare prefix",
        "*X25519:prime256v1",
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp256r1 },
        2,
        { 2 },
        1,
        { OSSL_TLS_GROUP_ID_x25519 },
        1,
    },

    /* --- #31315: removal that empties a *closed* tuple --------------- */
    {
        /*
         * -X25519 empties closed tuple 0; it must be excised and the
         * active-tuple counter shifted down (was: "1 group, 0 tuples").
         */
        "remove empties closed tuple (excision)",
        "X25519/prime256v1:-X25519",
        { OSSL_TLS_GROUP_ID_secp256r1 },
        1,
        { 1 },
        1,
        { KS_FIRST },
        1,
    },
    {
        /*
         * Removed group carried the keyshare and its tuple empties: the
         * keyshare must be dropped, not floated onto another tuple's group
         * (was: "1 group, tuples {1,1}", keyshare pointing at prime256v1).
         */
        "remove keyshared group empties tuple (drop, not float)",
        "*X25519/prime256v1/-X25519",
        { OSSL_TLS_GROUP_ID_secp256r1 },
        1,
        { 1 },
        1,
        { KS_FIRST },
        1,
    },
    {
        /*
         * Two removals, each emptying a distinct closed tuple (was:
         * "3 groups but tuple counts summing to 5").
         */
        "two removals empty two closed tuples",
        "X25519/secp256r1:secp384r1:secp521r1/*X448:-X25519/-X448",
        { OSSL_TLS_GROUP_ID_secp256r1, OSSL_TLS_GROUP_ID_secp384r1,
            OSSL_TLS_GROUP_ID_secp521r1 },
        3,
        { 3 },
        1,
        { KS_FIRST },
        1,
    },

    /* --- Removal from the *active* tuple (no excision) --------------- */
    {
        "remove empties the active tuple only",
        "X25519:-X25519",
        { 0 },
        0,
        { 0 },
        0,
        { 0 },
        0,
    },
    {
        "closed tuple intact, active tuple emptied and discarded",
        "X25519/prime256v1:-prime256v1",
        { OSSL_TLS_GROUP_ID_x25519 },
        1,
        { 1 },
        1,
        { KS_FIRST },
        1,
    },
    {
        "keyshared group removed from active tuple",
        "X25519/secp384r1/*X448:-X448",
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp384r1 },
        2,
        { 1, 1 },
        2,
        { KS_FIRST },
        1,
    },

    /* --- Legitimate keyshare retention (tuple not emptied) ----------- */
    {
        /*
         * Removing the keyshared X25519 from a tuple that still has
         * prime256v1: prime256v1's own keyshare is retained.
         */
        "remove one of two keyshares, tuple survives",
        "*X25519:*prime256v1:-X25519",
        { OSSL_TLS_GROUP_ID_secp256r1 },
        1,
        { 1 },
        1,
        { OSSL_TLS_GROUP_ID_secp256r1 },
        1,
    },

    /* --- Keyshare floats *within* its own (surviving, closed) tuple --- */
    {
        /*
         * X25519 carried the keyshare in closed tuple 0 {X25519,secp384r1};
         * removing it must float the keyshare to secp384r1 (the remaining
         * group of tuple 0), NOT to secp256r1 which is in tuple 1.
         */
        "keyshare floats to remaining group of same tuple",
        "*X25519:secp384r1/secp256r1:-X25519",
        { OSSL_TLS_GROUP_ID_secp384r1, OSSL_TLS_GROUP_ID_secp256r1 },
        2,
        { 1, 1 },
        2,
        { OSSL_TLS_GROUP_ID_secp384r1 },
        1,
    },
    {
        /*
         * Removed keyshared group is in the middle of tuple 0; its keyshare
         * floats to the tuple's first group (X25519), and tuple 1's own
         * keyshare (secp256r1) is untouched.  A float that escaped tuple 0
         * would corrupt this to a different keyshare set.
         */
        "mid-tuple keyshare floats to tuple head, not across tuples",
        "X25519:*X448:secp384r1 / *secp256r1:-X448",
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp384r1,
            OSSL_TLS_GROUP_ID_secp256r1 },
        3,
        { 2, 1 },
        2,
        { OSSL_TLS_GROUP_ID_x25519, OSSL_TLS_GROUP_ID_secp256r1 },
        2,
    },
};

/*
 * Assert every structural invariant the parser must maintain, from the parsed
 * state alone (independent of the specific input):
 *
 *  1. Partition:   sum(tuples[0..tuples_len)) == supportedgroups_len.
 *  2. No empty tuples survive the final compaction (every count > 0).
 *  3. Groups are distinct.
 *  4. Keyshares are either the lone "first group" sentinel {0}, or a set of
 *     distinct non-zero group IDs that appear as an ordered subsequence of
 *     the supported groups (never the sentinel mixed with real IDs).
 *
 * These are exactly the properties that were violated by GitHub #31315 (a
 * broken partition led to an out-of-bounds read in the remove path).
 */
static int check_invariants(SSL_CTX *ctx)
{
    size_t i, j, sum;
    int ok = 1;

    /* 1 + 2: partition, with no zero-count tuple left behind. */
    for (i = 0, sum = 0; i < ctx->ext.tuples_len; i++) {
        if (!TEST_size_t_gt(ctx->ext.tuples[i], 0)) {
            TEST_error("zero-count tuple at index %zu survived", i);
            ok = 0;
        }
        sum += ctx->ext.tuples[i];
    }
    if (!TEST_size_t_eq(sum, ctx->ext.supportedgroups_len))
        ok = 0;

    /* 3: groups distinct. */
    for (i = 0; i < ctx->ext.supportedgroups_len; i++)
        for (j = 0; j < i; j++)
            if (!TEST_uint_ne(ctx->ext.supportedgroups[i],
                    ctx->ext.supportedgroups[j]))
                ok = 0;

    /* 4: keyshare shape. */
    if (ctx->ext.keyshares_len == 1 && ctx->ext.keyshares[0] == KS_FIRST) {
        /* Sentinel form: a single implicit keyshare from the first group. */
    } else {
        size_t g = 0;

        for (i = 0; i < ctx->ext.keyshares_len; i++) {
            uint16_t ks = ctx->ext.keyshares[i];

            if (!TEST_uint_ne(ks, KS_FIRST)) { /* sentinel must be alone */
                ok = 0;
                continue;
            }
            for (j = 0; j < i; j++) /* distinct */
                if (!TEST_uint_ne(ks, ctx->ext.keyshares[j]))
                    ok = 0;
            while (g < ctx->ext.supportedgroups_len
                && ctx->ext.supportedgroups[g] != ks)
                g++; /* ordered subsequence */
            if (!TEST_size_t_lt(g, ctx->ext.supportedgroups_len)) {
                TEST_error("keyshare 0x%04X not an in-order group", ks);
                ok = 0;
                break;
            }
            g++;
        }
    }

    return ok;
}

static int run_case(int idx)
{
    const TESTCASE *tc = &cases[idx];
    SSL_CTX *ctx = NULL;
    int ret = 0;
    size_t i;

    TEST_info("case %d: %s  [\"%s\"]", idx, tc->desc, tc->list);

    if (!TEST_ptr(ctx = SSL_CTX_new(TLS_method())))
        goto end;

    if (!TEST_int_eq(SSL_CTX_set1_groups_list(ctx, tc->list), 1))
        goto end;

    /* Groups: exact contents and order. */
    if (!TEST_size_t_eq(ctx->ext.supportedgroups_len, tc->ngroups))
        goto end;
    for (i = 0; i < tc->ngroups; i++)
        if (!TEST_uint_eq(ctx->ext.supportedgroups[i], tc->groups[i]))
            goto end;

    /* Tuples: exact per-tuple counts. */
    if (!TEST_size_t_eq(ctx->ext.tuples_len, tc->ntuples))
        goto end;
    for (i = 0; i < tc->ntuples; i++)
        if (!TEST_size_t_eq(ctx->ext.tuples[i], tc->tuples[i]))
            goto end;

    /* Keyshares: exact contents (KS_FIRST == 0 sentinel). */
    if (!TEST_size_t_eq(ctx->ext.keyshares_len, tc->nkeyshares))
        goto end;
    for (i = 0; i < tc->nkeyshares; i++)
        if (!TEST_uint_eq(ctx->ext.keyshares[i], tc->keyshares[i]))
            goto end;

    if (!check_invariants(ctx))
        goto end;

    ret = 1;
end:
    SSL_CTX_free(ctx);
    return ret;
}

/*
 * Synthetic edge-case forms.  We do not spell out the exact parsed result for
 * these (that would just re-derive the parser); instead we assert the parse
 * succeeds or fails as expected and, on success, that all invariants hold.
 * These deliberately stress the corners of the remove/dedup/keyshare paths.
 */
typedef struct {
    const char *list;
    int expect_ok; /* 1: parse succeeds; 0: syntax/parse error */
} EDGECASE;

static const EDGECASE edgecases[] = {
    /* --- valid, invariant-preserving corner cases --- */
    { "X25519", 1 },
    { "X25519:secp256r1:secp384r1:secp521r1:X448", 1 }, /* one full tuple */
    { "X25519/secp256r1/secp384r1/secp521r1/X448", 1 }, /* many tuples */
    { "*X25519:secp256r1", 1 },
    { "X25519:X25519", 1 }, /* dup within tuple */
    { "X25519/X25519", 1 }, /* dup across tuples */
    { "X25519:-secp384r1", 1 }, /* remove absent: no-op */
    { "X25519:-X25519", 1 }, /* empty the active tuple */
    { "X25519/secp256r1:-X25519", 1 }, /* excise closed tuple */
    { "X25519/secp256r1/secp384r1:-secp256r1", 1 }, /* excise middle tuple */
    { "X25519:secp256r1/secp384r1:-secp384r1", 1 }, /* empty active, discard */
    { "*X25519/prime256v1/-X25519", 1 }, /* #31315: drop, not float */
    { "X25519/secp256r1:secp384r1:secp521r1/*X448:-X25519/-X448", 1 }, /* #31315 */
    { "*X25519:*secp256r1:-X25519", 1 }, /* keyshare survives sibling */
    { "X25519/secp256r1:-X25519:secp384r1", 1 }, /* excise then refill */
    { "X25519:secp256r1:X448:-X25519:-X448", 1 }, /* multiple removals */
    { "?*BOGUS:X25519 / *secp256r1", 1 }, /* stacked prefix, unknown */
    { "X25519:?BOGUS:secp256r1", 1 }, /* ignore unknown mid-tuple */
    { "*X25519:DEFAULT:-secp256r1:-X448", 1 }, /* DEFAULT + removals */
    { "DEFAULT:-X25519:-?curveSM2:-?ffdhe2048:-?ffdhe3072", 1 },
    { "secp256r1:DEFAULT", 1 }, /* prepend then DEFAULT */

    /* --- expected syntax / parse errors --- */
    { "X25519//secp256r1", 0 }, /* empty tuple */
    { "X25519::secp256r1", 0 }, /* empty group */
    { ":X25519", 0 },
    { "X25519:", 0 },
    { "/X25519", 0 },
    { "X25519/", 0 },
    { "**X25519", 0 }, /* double keyshare prefix */
    { "??X25519", 0 },
    { "--X25519", 0 },
    { "X25519:NOTAREALGROUP", 0 }, /* unknown w/o '?' */
    { "-DEFAULT", 0 }, /* prefix on pseudo-group */
    { "?DEFAULT", 0 },
};

static int run_edge(int idx)
{
    const EDGECASE *tc = &edgecases[idx];
    SSL_CTX *ctx = NULL;
    int ret = 0, r;

    TEST_info("edge %d: [\"%s\"] expect %s", idx, tc->list,
        tc->expect_ok ? "ok" : "error");

    if (!TEST_ptr(ctx = SSL_CTX_new(TLS_method())))
        goto end;

    r = SSL_CTX_set1_groups_list(ctx, tc->list);
    if (tc->expect_ok) {
        if (!TEST_int_eq(r, 1) || !check_invariants(ctx))
            goto end;
    } else {
        if (!TEST_int_eq(r, 0))
            goto end;
    }

    ret = 1;
end:
    SSL_CTX_free(ctx);
    return ret;
}

int setup_tests(void)
{
    ADD_ALL_TESTS(run_case, (int)OSSL_NELEM(cases));
    ADD_ALL_TESTS(run_edge, (int)OSSL_NELEM(edgecases));
    return 1;
}