Fix group tuple handling in DEFAULT expansion

Also fine-tune docs and add tests.

Fixes: #30109
Fixes: CVE-2026-2673

Reviewed-by: Tim Hudson <tjh@openssl.org>
Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
MergeDate: Wed Feb 25 11:08:03 2026
(Merged from https://github.com/openssl/openssl/pull/30113)
This commit is contained in:
Viktor Dukhovni 2026-02-15 22:50:09 +11:00
parent 2af8d3b640
commit 9efd7e9e98
3 changed files with 231 additions and 120 deletions

View file

@ -40,13 +40,13 @@ SSL_get1_curves, SSL_get_shared_curve, SSL_CTX_get0_implemented_groups
For all of the functions below that set the supported groups there must be at
least one group in the list. A number of these functions identify groups via a
unique integer NID value. However, support for some groups may be added by
external providers. In this case there will be no NID assigned for the group.
unique integer B<NID> value. However, support for some groups may be added by
external providers. In this case there will be no B<NID> assigned for the group.
When setting such groups applications should use the "list" form of these
functions (i.e. SSL_CTX_set1_groups_list() and SSL_set1_groups_list()).
SSL_CTX_set1_groups() sets the supported groups for B<ctx> to B<glistlen>
groups in the array B<glist>. The array consist of all NIDs of supported groups.
groups in the array B<glist>. The array consist of all B<NIDs> of supported groups.
The supported groups for B<TLSv1.3> include:
B<NID_X9_62_prime256v1>,
B<NID_secp384r1>,
@ -73,20 +73,27 @@ B<SSL_OP_SERVER_PREFERENCE> is set, the order of the elements in the
array determines the selected group. Otherwise, the order is ignored and the
client's order determines the selection.
For a TLS 1.3 server, the groups determine the selected group, but
selection is more complex. A TLS 1.3 client sends both a group list as well as a
predicted subset of groups. Choosing a group outside the predicted subset incurs
an extra roundtrip. However, in some situations, the most preferred group may
not be predicted. OpenSSL considers all supported groups in I<clist> to be comparable
in security and prioritizes avoiding roundtrips above either client or server
preference order. If an application uses an external provider to extend OpenSSL
with, e.g., a post-quantum algorithm, this behavior may allow a network attacker
to downgrade connections to a weaker algorithm. It is therefore recommended
to use SSL_CTX_set1_groups_list() with the ability to specify group tuples.
For a TLS 1.3 server, the groups determine the selected group, but selection is
more complex.
A TLS 1.3 client sends both a group list and predicted keyshares for a subset
of groups.
A server choosing a group outside the client's predicted subset incurs an extra
roundtrip.
However, in some situations, the most preferred group may not be predicted.
When groups are specified via SSL_CTX_set1_groups() as a list of B<NID>
values, OpenSSL considers all supported groups in I<clist> to be comparable in
security and prioritises avoiding roundtrips above either client or server
preference order.
If an application uses an external provider to extend OpenSSL with, e.g., a
post-quantum algorithm, this behavior may allow a network attacker to downgrade
connections to a weaker algorithm.
It is therefore recommended to use SSL_CTX_set1_groups_list() instead, making
it possible to specify group tuples as described below.
SSL_CTX_set1_groups_list() sets the supported groups for B<ctx> to
string I<list>. In contrast to SSL_CTX_set1_groups(), the names of the
groups, rather than their NIDs, are used.
groups, rather than their B<NIDs>, are used.
The commands below list the available groups for TLS 1.2 and TLS 1.3,
respectively:
@ -102,30 +109,72 @@ The preferred group names are those defined by
L<IANA|https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8>.
The I<list> can be used to define several group tuples of comparable security
levels, and can specify which key shares should be sent by a client.
The specified list elements can optionally be ignored, if not implemented
levels, and can specify which predicted key shares should be sent by a client.
Group tuples are used by OpenSSL TLS servers to decide whether to request a
stronger keyshare than those predicted by sending a Hello Retry Request
(B<HRR>) even if some of the predicted groups are supported.
OpenSSL clients ignore tuple boundaries, and pay attenion only to the overall
order of I<list> elements and which groups are selected as predicted keyshares
as described below.
The specified list elements can optionally be ignored if not implemented
(listing unknown groups otherwise results in error).
It is also possible to specify the built-in default set of groups, and to explicitly
remove a group from that list.
It is also possible to specify the built-in default set of groups, and to
explicitly remove a group from that list.
In its simplest form, the string I<list> is just a colon separated list
of group names, for example "P-521:P-384:P-256:X25519:ffdhe2048". The first
group listed will also be used for the B<key_share> sent by a client in a
TLSv1.3 B<ClientHello>. For servers note the discussion above. The list should
be in order of preference with the most preferred group first.
In its simplest legacy form, the string I<list> is just a colon separated list
of group names, for example "P-521:P-384:P-256:X25519:ffdhe2048".
The first group listed will in this case be used as the sole predicted
B<key_share> sent by a client in a TLSv1.3 B<ClientHello>.
The list should be in order of preference with the most preferred group first.
Group tuples of comparable security are defined by separating them from each
other by a tuple separator C</>. Keyshares to be sent by a client are specified
by prepending a C<*> to the group name, while any C<*> will be ignored by a
server. The following string I<list> for example defines three tuples when
used on the server-side, and triggers the generation of three key shares
when used on the client-side: P-521:*P-256/*P-384/*X25519:P-384:ffdhe2048.
A more expressive syntax supports definition of group tuples of comparable
security by separating them from each other with C</> characters.
If a group name is preceded with the C<?> character, it will be ignored if an
implementation is missing. If a group name is preceded with the C<-> character, it
will be removed from the list of groups if present (including not sending a
key share for this group), ignored otherwise. The pseudo group name
C<DEFAULT> can be used to select the OpenSSL built-in default list of groups.
The predicted keyshares to be sent by clients can be explicitly specified by
adding a C<*> prefix to the associated group name.
These C<*> prefixes are ignored by servers.
If a group name is prefixed with the C<?> character, it will be ignored if an
implementation is missing.
Otherwise, listing an unknown group name will cause a failure to parse the
I<list>.
Note that whether a group is known or not may depend on the OpenSSL version,
how OpenSSL was compiled and/or which providers are loaded.
Make sure you have the correct spelling of the group name and when in doubt
prefix it with a C<?> to handle configurations in which it might nevertheless
be unknown.
If a group name is prefixed with the C<-> character, it will be removed from
the list of groups specified up to that point.
It can be added again if specified later.
Removal of groups that have not been included earlier in the list is silently
ignored.
The pseudo group name C<DEFAULT> can be used to select the OpenSSL built-in
default list of groups.
Prepending one or more groups to C<DEFAULT> using only C<:> separators prepends those
groups to the built-in default list's first tuple.
Additional tuples can be prepended by use of the C</> separator.
Appending a set of groups to C<DEFAULT> using only C<:> separators appends those
groups to the built-in default list's last tuple.
Additional tuples can be appended by use of the C</> separator.
The B<DEFAULT> list selects B<X25519MLKEM768> as one of the predicted keyshares.
In rare cases this can lead to failures or timeouts because the resulting
larger TLS Client Hello message may no longer fit in a single TCP segment and
firewall software may erroneously disrupt the TLS handshake.
If this is an issue or concern, prepending C<?X25519MLKEM768:> without a C<*>
prefix leads to its occurrence in the default list to be ignored as a duplicate,
and along with that also the keyshare prediction.
The group will then only be selected by servers that specifically expect it,
after a Hello Retry Request (HRR).
Servers that specifically prefer B<X25519MLKEM768>, are much less likely to be
found behind problematic firewalls.
The following string I<list> for example defines three tuples when used on the
server-side, and triggers the generation of three key shares when used on the
client-side: P-521:*P-256/*P-384/*X25519:P-384:ffdhe2048.
For a TLS 1.3 client, all the groups in the string I<list> are added to the
supported groups extension of a C<ClientHello>, in the order in which they are listed,

View file

@ -225,7 +225,7 @@ static const uint16_t suiteb_curves[] = {
/* Group list string of the built-in pseudo group DEFAULT_SUITE_B */
#define SUITE_B_GROUP_NAME "DEFAULT_SUITE_B"
#define SUITE_B_GROUP_LIST "secp256r1:secp384r1",
#define SUITE_B_GROUP_LIST "?secp256r1:?secp384r1",
struct provider_ctx_data_st {
SSL_CTX *ctx;
@ -1274,8 +1274,8 @@ typedef struct {
size_t ksidcnt; /* Number of key shares */
uint16_t *ksid_arr; /* The IDs of the key share groups (flat list) */
/* Variable to keep state between execution of callback or helper functions */
size_t tuple_mode; /* Keeps track whether tuple_cb called from 'the top' or from gid_cb */
int ignore_unknown_default; /* Flag such that unknown groups for DEFAULT[_XYZ] are ignored */
int inner; /* Are we expanding a DEFAULT list */
int first; /* First tuple of possibly nested expansion? */
} gid_cb_st;
/* Forward declaration of tuple callback function */
@ -1350,16 +1350,16 @@ static int gid_cb(const char *elem, int len, void *arg)
for (i = 0; i < OSSL_NELEM(default_group_strings); i++) {
if ((size_t)len == (strlen(default_group_strings[i].list_name))
&& OPENSSL_strncasecmp(default_group_strings[i].list_name, elem, len) == 0) {
int saved_first;
/*
* We're asked to insert an entire list of groups from a
* DEFAULT[_XYZ] 'pseudo group' which we do by
* recursively calling this function (indirectly via
* CONF_parse_list and tuple_cb); essentially, we treat a DEFAULT
* group string like a tuple which is appended to the current tuple
* rather then starting a new tuple. Variable tuple_mode is the flag which
* controls append tuple vs start new tuple.
* rather then starting a new tuple.
*/
if (ignore_unknown || remove_group)
return -1; /* removal or ignore not allowed here -> syntax error */
@ -1380,15 +1380,17 @@ static int gid_cb(const char *elem, int len, void *arg)
default_group_strings[i].group_string,
strlen(default_group_strings[i].group_string));
restored_default_group_string[strlen(default_group_strings[i].group_string) + restored_prefix_index] = '\0';
/* We execute the recursive call */
garg->ignore_unknown_default = 1; /* We ignore unknown groups for DEFAULT_XYZ */
/* we enforce group mode (= append tuple) for DEFAULT_XYZ group lists */
garg->tuple_mode = 0;
/* We use the tuple_cb callback to process the pseudo group tuple */
/*
* Append first tuple of result to current tuple, and don't
* terminate the last tuple until we return to a top-level
* tuple_cb.
*/
saved_first = garg->first;
garg->inner = garg->first = 1;
retval = CONF_parse_list(restored_default_group_string,
TUPLE_DELIMITER_CHARACTER, 1, tuple_cb, garg);
garg->tuple_mode = 1; /* next call to tuple_cb will again start new tuple */
garg->ignore_unknown_default = 0; /* reset to original value */
garg->inner = 0;
garg->first = saved_first;
/* We don't need the \0-terminated string anymore */
OPENSSL_free(restored_default_group_string);
@ -1408,9 +1410,6 @@ static int gid_cb(const char *elem, int len, void *arg)
if (len == 0)
return -1; /* Seems we have prefxes without a group name -> syntax error */
if (garg->ignore_unknown_default == 1) /* Always ignore unknown groups for DEFAULT[_XYZ] */
ignore_unknown = 1;
/* Memory management in case more groups are present compared to initial allocation */
if (garg->gidcnt == garg->gidmax) {
uint16_t *tmp = OPENSSL_realloc_array(garg->gid_arr,
@ -1486,51 +1485,48 @@ static int gid_cb(const char *elem, int len, void *arg)
}
/* Remove group (and keyshare) from anywhere in the list if present, ignore if not present */
if (remove_group) {
/* Is the current group specified anywhere in the entire list so far? */
found_group = 0;
for (i = 0; i < garg->gidcnt; i++)
if (garg->gid_arr[i] == gid) {
found_group = 1;
size_t n; /* tuple size */
j = 0; /* tuple index */
k = 0; /* keyshare index */
n = garg->tuplcnt_arr[j];
for (i = 0; i < garg->gidcnt; ++i) {
if (garg->gid_arr[i] == gid)
break;
}
/* The group to remove is at position i in the list of (zero indexed) groups */
if (found_group) {
/* We remove that group from its position (which is at i)... */
for (j = i; j < (garg->gidcnt - 1); j++)
garg->gid_arr[j] = garg->gid_arr[j + 1]; /* ...shift remaining groups left ... */
garg->gidcnt--; /* ..and update the book keeping for the number of groups */
/* Skip keyshare slots associated with groups prior to that removed */
if (k < garg->ksidcnt && garg->gid_arr[i] == garg->ksid_arr[k])
++k;
/* Skip to next tuple? */
if (j < garg->tplcnt && --n == 0)
n = garg->tuplcnt_arr[++j];
}
/*
* We also must update the number of groups either in a previous tuple (which we
* must identify and check whether it becomes empty due to the deletion) or in
* the current tuple, pending where the deleted group resides
*/
k = 0;
for (j = 0; j < garg->tplcnt; j++) {
k += garg->tuplcnt_arr[j];
/* Remark: i is zero-indexed, k is one-indexed */
if (k > i) { /* remove from one of the previous tuples */
garg->tuplcnt_arr[j]--;
break; /* We took care not to have group duplicates, hence we can stop here */
}
}
if (k <= i) /* remove from current tuple */
garg->tuplcnt_arr[j]--;
/* Nothing to remove? */
if (i >= garg->gidcnt)
goto done;
/* We also remove the group from the list of keyshares (if present) */
found_group = 0;
for (i = 0; i < garg->ksidcnt; i++)
if (garg->ksid_arr[i] == gid) {
found_group = 1;
break;
}
if (found_group) {
/* Found, hence we remove that keyshare from its position (which is at i)... */
for (j = i; j < (garg->ksidcnt - 1); j++)
garg->ksid_arr[j] = garg->ksid_arr[j + 1]; /* shift remaining key shares */
/* ... and update the book keeping */
garg->ksidcnt--;
}
garg->gidcnt--;
garg->tuplcnt_arr[j]--;
memmove(garg->gid_arr + i, garg->gid_arr + i + 1,
(garg->gidcnt - i) * sizeof(gid));
/* Handle keyshare removal */
if (k < garg->ksidcnt && garg->ksid_arr[k] == gid) {
garg->ksidcnt--;
memmove(garg->ksid_arr + k, garg->ksid_arr + k + 1,
(garg->ksidcnt - k) * sizeof(gid));
}
/*
* Adjust closed or current tuple's group count, if a closed tuple
* count reaches zero excise the resulting empty tuple. The current
* (not yet closed) tuple at the end of the list stays even if empty.
*/
if (garg->tuplcnt_arr[j] == 0 && j < garg->tplcnt) {
garg->tplcnt--;
memmove(garg->tuplcnt_arr + j, garg->tuplcnt_arr + j + 1,
(garg->tplcnt - j) * sizeof(size_t));
}
} else { /* Processing addition of a single new group */
@ -1546,7 +1542,7 @@ static int gid_cb(const char *elem, int len, void *arg)
/* and update the book keeping for the number of groups in current tuple */
garg->tuplcnt_arr[garg->tplcnt]++;
/* We memorize if needed that we want to add a key share for the current group */
/* We want to add a key share for the current group */
if (add_keyshare)
garg->ksid_arr[garg->ksidcnt++] = gid;
}
@ -1555,6 +1551,34 @@ done:
return retval;
}
static int grow_tuples(gid_cb_st *garg)
{
if (garg->tplcnt == garg->tplmax) {
size_t *tmp = OPENSSL_realloc_array(garg->tuplcnt_arr,
garg->tplmax + GROUPLIST_INCREMENT,
sizeof(*garg->tuplcnt_arr));
if (tmp == NULL)
return 0;
garg->tplmax += GROUPLIST_INCREMENT;
garg->tuplcnt_arr = tmp;
}
return 1;
}
static int close_tuple(gid_cb_st *garg)
{
size_t gidcnt = garg->tuplcnt_arr[garg->tplcnt];
if (gidcnt == 0)
return 1;
if (!grow_tuples(garg))
return 0;
garg->tuplcnt_arr[++garg->tplcnt] = 0;
return 1;
}
/* Extract and process a tuple of groups */
static int tuple_cb(const char *tuple, int len, void *arg)
{
@ -1568,17 +1592,9 @@ static int tuple_cb(const char *tuple, int len, void *arg)
return 0;
}
/* Memory management for tuples */
if (garg->tplcnt == garg->tplmax) {
size_t *tmp = OPENSSL_realloc_array(garg->tuplcnt_arr,
garg->tplmax + GROUPLIST_INCREMENT,
sizeof(*garg->tuplcnt_arr));
if (tmp == NULL)
return 0;
garg->tplmax += GROUPLIST_INCREMENT;
garg->tuplcnt_arr = tmp;
}
if (garg->inner && !garg->first && !close_tuple(garg))
return 0;
garg->first = 0;
/* Convert to \0-terminated string */
restored_tuple_string = OPENSSL_malloc(len + 1 /* \0 */);
@ -1593,15 +1609,8 @@ static int tuple_cb(const char *tuple, int len, void *arg)
/* We don't need the \o-terminated string anymore */
OPENSSL_free(restored_tuple_string);
if (garg->tuplcnt_arr[garg->tplcnt] > 0) { /* Some valid groups are present in current tuple... */
if (garg->tuple_mode) {
/* We 'close' the tuple */
garg->tplcnt++;
garg->tuplcnt_arr[garg->tplcnt] = 0; /* Next tuple is initialized to be empty */
garg->tuple_mode = 1; /* next call will start a tuple (unless overridden in gid_cb) */
}
}
if (!garg->inner && !close_tuple(garg))
return 0;
return retval;
}
@ -1632,8 +1641,6 @@ int tls1_set_groups_list(SSL_CTX *ctx,
}
memset(&gcb, 0, sizeof(gcb));
gcb.tuple_mode = 1; /* We prepare to collect the first tuple */
gcb.ignore_unknown_default = 0;
gcb.gidmax = GROUPLIST_INCREMENT;
gcb.tplmax = GROUPLIST_INCREMENT;
gcb.ksidmax = GROUPLIST_INCREMENT;

View file

@ -40,6 +40,12 @@ typedef enum SERVER_RESPONSE {
SH = 2
} SERVER_RESPONSE;
static const char *response_desc[] = {
"HRR",
"INIT",
"SH",
};
static char *cert = NULL;
static char *privkey = NULL;
@ -51,6 +57,17 @@ struct tls13groupselection_test_st {
const enum SERVER_RESPONSE expected_server_response;
};
/*
* Tests that probe robust handling of group removal depend on detailed
* knowledge of the default group list. A stable list is needed that does not
* depend on future changes in the actual built-in default.
*/
#define TEST_DEFLT \
"?*X25519MLKEM768 / " \
"?*X25519 : ?secp256r1 / " \
"?X448 : ?secp384r1 : ?secp521r1 / " \
"?ffdhe2048:?ffdhe3072"
static const struct tls13groupselection_test_st tls13groupselection_tests[] = {
/*
@ -313,7 +330,40 @@ static const struct tls13groupselection_test_st tls13groupselection_tests[] = {
{ "*brainpoolP256r1:X25519", /* test 43 */
"X25519",
SERVER_PREFERENCE,
NEGOTIATION_FAILURE, INIT }
NEGOTIATION_FAILURE, INIT },
/* DEFAULT retains tuple structure */
{ "*X25519:secp256r1",
"secp256r1:DEFAULT", /* test 44 */
SERVER_PREFERENCE,
"secp256r1", HRR },
#ifndef OPENSSL_NO_DH
{ "*ffdhe2048:secp256r1",
"DEFAULT:ffdhe4096", /* test 45 */
CLIENT_PREFERENCE,
"secp256r1", HRR },
{ "x25519:ffdhe2048:*ffdhe4096",
"DEFAULT:ffdhe4096", /* test 46 */
SERVER_PREFERENCE,
"x25519", HRR },
/*
* The server's second tuple becomes empty after removal
* of "secp256r1", the subsequent removal of X448 is
* then from the third tuple.
*/
{ "*ffdhe2048:secp384r1", /* test 47 */
"*X25519:" TEST_DEFLT ":-secp256r1:-X448",
SERVER_PREFERENCE,
"secp384r1", HRR },
/*
* The server's last tuple becomes empty after removals,
* and then continues to fill.
*/
{ "*ffdhe2048:ffdhe4096", /* test 48 */
"*X25519:" TEST_DEFLT ":-ffdhe2048:-ffdhe3072:ffdhe4096",
SERVER_PREFERENCE,
"ffdhe4096", HRR },
#endif
};
static void server_response_check_cb(int write_p, int version,
@ -324,10 +374,12 @@ static void server_response_check_cb(int write_p, int version,
enum SERVER_RESPONSE *server_response = (enum SERVER_RESPONSE *)arg;
/* Prepare check for HRR */
const uint8_t *incoming_random = (const uint8_t *)buf + 6;
const uint8_t magic_HRR_random[32] = { 0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11,
const uint8_t magic_HRR_random[32] = {
0xCF, 0x21, 0xAD, 0x74, 0xE5, 0x9A, 0x61, 0x11,
0xBE, 0x1D, 0x8C, 0x02, 0x1E, 0x65, 0xB8, 0x91,
0xC2, 0xA2, 0x11, 0x16, 0x7A, 0xBB, 0x8C, 0x5E,
0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C };
0x07, 0x9E, 0x09, 0xE2, 0xC8, 0xA8, 0x33, 0x9C
};
/* Did a server hello arrive? */
if (write_p == 0 && /* Incoming data... */
@ -456,13 +508,16 @@ static int test_groupnegotiation(const struct tls13groupselection_test_st *curre
group_name_client = SSL_group_to_name(clientssl, negotiated_group_client);
if (!TEST_int_eq(negotiated_group_client, negotiated_group_server))
goto end;
if (!TEST_int_eq((int)current_test_vector->expected_server_response, (int)server_response))
if (!TEST_str_eq(response_desc[current_test_vector->expected_server_response],
response_desc[server_response]))
goto end;
if (TEST_str_eq(group_name_client, current_test_vector->expected_group))
ok = 1;
} else {
TEST_false_or_end(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE));
if (test_type == TEST_NEGOTIATION_FAILURE && !TEST_int_eq((int)current_test_vector->expected_server_response, (int)server_response))
if (test_type == TEST_NEGOTIATION_FAILURE
&& !TEST_str_eq(response_desc[current_test_vector->expected_server_response],
response_desc[server_response]))
goto end;
ok = 1;
}