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();
         }
     }