Commit debe2dbe90 for openssl.org
commit debe2dbe905fef5d895526ec13924f1c74519d80
Author: Paul Grubbs <paulgrub@umich.edu>
Date: Wed Aug 26 14:36:18 2026 -0400
Enforce the supported_groups/key_share pairing in TLS 1.3 ClientHellos
RFC 8446, section 9.2, lists the requirements a ClientHello that offers
TLS 1.3 must meet, including:
If containing a "supported_groups" extension, it MUST also contain a
"key_share" extension, and vice versa. An empty KeyShare.client_shares
vector is permitted.
Servers receiving a ClientHello which does not conform to the
requirements in this section MUST abort the handshake with a
"missing_extension" alert.
The server only enforced half of this: tls_parse_ctos_key_share() rejects
a key_share without supported_groups (except when resuming with the
psk_dhe_ke mode not offered, where it returns early), but nothing checked
the other direction. A PSK resumption ClientHello that carries
supported_groups and psk_key_exchange_modes = {psk_ke, psk_dhe_ke} but
no key_share was therefore accepted, and resumed with psk_ke, whenever
the server allows non-DHE PSK key exchange (SSL_OP_ALLOW_NO_DHE_KEX).
Check both directions at the start of the server branch of
final_key_share(), which runs for every TLS 1.3 ClientHello (including
the one following a HelloRetryRequest) after all extensions have been
parsed, using the extension presence recorded in the pre-processed
ClientHello like final_psk() already does for psk_key_exchange_modes.
A ClientHello with neither extension is still accepted, as permitted
for a PSK-only ClientHello.
The rule in section 4.2.9 that a client offering psk_dhe_ke supply a
key_share is deliberately not enforced here since that text is about
the mode the server selects rather than a well-formedness requirement.
Add two cases to 70-test_tls13kexmodes.t: a resumption ClientHello with
both kex modes and supported_groups but no key_share must now fail with
a missing_extension alert, and one with psk_ke only and neither
extension must still resume.
Fixes #25124
Assisted-by: Claude:claude-fable-5
Reviewed-by: Frederik Wedel-Heinen <fwh.openssl@gmail.com>
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Merge-date: Fri Oct 2 08:40:59 2026
Merged-from: https://github.com/openssl/openssl/pull/32529
diff --git a/CHANGES.md b/CHANGES.md
index 5015ab5378..2d61b97ac2 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -87,6 +87,15 @@ OpenSSL 4.2
*Dominic Cunningham, Billy Bob Brumley*
+ * The TLS 1.3 server now enforces the RFC 8446 section 9.2 requirement that
+ a ClientHello containing a supported_groups extension also contains a
+ key_share extension and vice versa, aborting the handshake with a
+ missing_extension alert otherwise. Previously a PSK resumption ClientHello
+ that offered psk_ke and carried supported_groups but no key_share was
+ accepted when the server allowed non-DHE PSK key exchange.
+
+ *Paul Grubbs*
+
OpenSSL 4.1
-----------
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index d09a43d616..2ce0db3e2b 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -1766,6 +1766,23 @@ static int final_key_share(SSL_CONNECTION *s, unsigned int context, int sent)
* send a HelloRetryRequest
*/
if (s->server) {
+ /*
+ * RFC 8446, section 9.2: a ClientHello containing a supported_groups
+ * extension MUST also contain a key_share extension, and vice versa
+ * (an empty KeyShare.client_shares vector is permitted). Servers
+ * MUST abort the handshake with a missing_extension alert otherwise.
+ */
+ if (s->clienthello != NULL) {
+ const RAW_EXTENSION *groups = &s->clienthello->pre_proc_exts[TLSEXT_IDX_supported_groups];
+
+ if ((sent != 0) != (groups->present != 0)) {
+ SSLfatal(s, SSL_AD_MISSING_EXTENSION,
+ sent ? SSL_R_MISSING_SUPPORTED_GROUPS_EXTENSION
+ : SSL_R_NO_SUITABLE_KEY_SHARE);
+ return 0;
+ }
+ }
+
if (s->s3.peer_tmp != NULL) {
/* We have a suitable key_share */
if ((s->s3.flags & TLS1_FLAGS_STATELESS) != 0
diff --git a/test/recipes/70-test_tls13kexmodes.t b/test/recipes/70-test_tls13kexmodes.t
index fab2505ad4..b7e3bb1c46 100644
--- a/test/recipes/70-test_tls13kexmodes.t
+++ b/test/recipes/70-test_tls13kexmodes.t
@@ -194,9 +194,11 @@ use constant {
NON_DHE_KEX_MODE_ONLY => 2,
DHE_KEX_MODE_ONLY => 3,
UNKNOWN_KEX_MODES => 4,
- BOTH_KEX_MODES => 5
+ BOTH_KEX_MODES => 5,
+ BOTH_KEX_MODES_NO_KEY_SHARE => 6,
+ NON_DHE_KEX_MODE_ONLY_NO_GROUPS => 7
};
-my $testcount = 13;
+my $testcount = 15;
$ENV{OPENSSL_MODULES} = abs_path(bldtop_dir("test"));
@@ -429,6 +431,35 @@ sub run_tests
$proxy->start();
ok(TLSProxy::Message->fail(), "Resume with dhe kex mode, no overlapping groups");
+ #Test 14: Attempt a resume with both non-dhe and dhe kex mode and a
+ # supported_groups extension but no key_share extension. Should fail
+ # (RFC 8446 section 9.2: a ClientHello containing supported_groups
+ # MUST also contain key_share), even though non-dhe resumption is
+ # allowed by the server
+ $proxy->clear();
+ $proxy->cipherc("DEFAULT:\@SECLEVEL=2");
+ $proxy->clientflags("-curves P-256:P-384:X25519:X448 -no_rx_cert_comp -allow_no_dhe_kex -sess_in " . $session);
+ $proxy->serverflags("-curves P-256:P-384:X25519:X448 -allow_no_dhe_kex");
+ $testtype = BOTH_KEX_MODES_NO_KEY_SHARE;
+ $proxy->start();
+ ok(TLSProxy::Message->fail()
+ && TLSProxy::Message->alert->description()
+ == TLSProxy::Message::AL_DESC_MISSING_EXTENSION,
+ "Resume with both kex modes, supported_groups but no key_share");
+
+ #Test 15: Attempt a resume with non-dhe kex mode only and with neither a
+ # supported_groups nor a key_share extension. Should resume without
+ # a key_share (omitting both extensions is permitted for a PSK-only
+ # ClientHello)
+ $proxy->clear();
+ $proxy->cipherc("DEFAULT:\@SECLEVEL=2");
+ $proxy->clientflags("-curves P-256:P-384:X25519:X448 -no_rx_cert_comp -allow_no_dhe_kex -sess_in " . $session);
+ $proxy->serverflags("-curves P-256:P-384:X25519:X448 -allow_no_dhe_kex");
+ $testtype = NON_DHE_KEX_MODE_ONLY_NO_GROUPS;
+ $proxy->start();
+ ok(TLSProxy::Message->success(),
+ "Resume with non-dhe kex mode, no supported_groups and no key_share");
+
unlink $session;
}
@@ -446,7 +477,8 @@ sub modify_kex_modes_filter
if ($testtype == EMPTY_EXTENSION) {
$ext = pack "C",
0x00; #List length
- } elsif ($testtype == NON_DHE_KEX_MODE_ONLY) {
+ } elsif ($testtype == NON_DHE_KEX_MODE_ONLY
+ || $testtype == NON_DHE_KEX_MODE_ONLY_NO_GROUPS) {
$ext = pack "C2",
0x01, #List length
0x00; #psk_ke
@@ -459,7 +491,8 @@ sub modify_kex_modes_filter
0x02, #List length
0xfe, #unknown
0xff; #unknown
- } elsif ($testtype == BOTH_KEX_MODES) {
+ } elsif ($testtype == BOTH_KEX_MODES
+ || $testtype == BOTH_KEX_MODES_NO_KEY_SHARE) {
#We deliberately list psk_ke first...should still use psk_dhe_ke, except if the server is configured otherwise.
$ext = pack "C3",
0x02, #List length
@@ -475,6 +508,15 @@ sub modify_kex_modes_filter
TLSProxy::Message::EXT_PSK_KEX_MODES, $ext);
}
+ if ($testtype == BOTH_KEX_MODES_NO_KEY_SHARE
+ || $testtype == NON_DHE_KEX_MODE_ONLY_NO_GROUPS) {
+ $message->delete_extension(TLSProxy::Message::EXT_KEY_SHARE);
+ }
+ if ($testtype == NON_DHE_KEX_MODE_ONLY_NO_GROUPS) {
+ $message->delete_extension(
+ TLSProxy::Message::EXT_SUPPORTED_GROUPS);
+ }
+
$message->repack();
}
}