Commit e9e0480e5d for openssl.org

commit e9e0480e5da9a374521e55aa2d5faeb1843dd313
Author: Viktor Dukhovni <openssl-users@dukhovni.org>
Date:   Wed Dec 17 03:48:06 2025 +1100

    Advertise FFDHE groups also with TLS 1.2-only

    When the TLS max version is TLS 1.2, include supported RFC7919 FFDHE
    groups in the supported_groups extension, provided we support at least
    one DHE key exchange ciphersuite.

    Also skip the EC point formats extension when the minimum (D)TLS version
    is greater than 1.2.  That extension is obsolete as of (D)TLS 1.3.

    Finally, folded some extant long lines from the previous RFC7919 commits.

    Reviewed-by: Matt Caswell <matt@openssl.org>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    MergeDate: Thu Feb  5 09:09:43 2026
    (Merged from https://github.com/openssl/openssl/pull/24551)

diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index a50ee6e1dc..0f6402b2b7 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -2801,7 +2801,7 @@ __owur int tls1_set_groups_list(SSL_CTX *ctx,
     const char *str);
 __owur EVP_PKEY *ssl_generate_pkey_group(SSL_CONNECTION *s, uint16_t id);
 __owur int tls_valid_group(SSL_CONNECTION *s, uint16_t group_id, int minversion,
-    int maxversion, int isec, int *okfortls13);
+    int maxversion, int *okfortls13, const TLS_GROUP_INFO **giptr);
 __owur EVP_PKEY *ssl_generate_param_group(SSL_CONNECTION *s, uint16_t id);
 void tls1_get_formatlist(SSL_CONNECTION *s, const unsigned char **pformats,
     size_t *num_formats);
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index e91b7400ea..6453b84fd7 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -11,8 +11,16 @@
 #include "../ssl_local.h"
 #include "internal/cryptlib.h"
 #include "internal/ssl_unwrap.h"
+#include "internal/tlsgroups.h"
 #include "statem_local.h"

+/* Used in the negotiate_dhe function */
+typedef enum {
+    ffdhe_check,
+    ecdhe_check,
+    ptfmt_check
+} dhe_check_t;
+
 EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt,
     unsigned int context, X509 *x,
     size_t chainidx)
@@ -136,43 +144,78 @@ EXT_RETURN tls_construct_ctos_srp(SSL_CONNECTION *s, WPACKET *pkt,
 }
 #endif

-static int use_ecc(SSL_CONNECTION *s, int min_version, int max_version)
+/*
+ * With (D)TLS < 1.3 the only negotiated supported key exchange groups are
+ * FFDHE (RFC7919) and ECDHE/ECX (RFC8422 + legacy).  With (D)TLS 1.3, we add
+ * KEMs, and the supported groups are no longer cipher-dependent.
+ *
+ * This function serves two purposes:
+ *
+ * - To determine whether to send the supported point formats extension.
+ *   This is no longer applicable with (D)TLS >= 1.3.
+ * - To determine whether to send the supported groups extension.
+ *
+ * In the former case, we only care about whether both ECC ciphers and EC/ECX
+ * supported groups are configured, and the (D)TLS min version is at most 1.2.
+ *
+ * In the latter case, we also admit DHE ciphers with FFDHE groups, or any TLS
+ * 1.3 cipher, since the extension is effectively mandatory for (D)TLS 1.3,
+ * with the sole exception of psk-ke resumption, provided the client is sure
+ * that the server will not want elect a full handshake. The check type then
+ * indicates whether ECDHE or FFDHE negotiation should be performed.
+ */
+static int negotiate_dhe(SSL_CONNECTION *s, dhe_check_t check_type,
+    int min_version, int max_version)
 {
     int i, end, ret = 0;
-    unsigned long alg_k, alg_a;
     STACK_OF(SSL_CIPHER) *cipher_stack = NULL;
     const uint16_t *pgroups = NULL;
     size_t num_groups, j;
     SSL *ssl = SSL_CONNECTION_GET_SSL(s);
+    int dtls = SSL_CONNECTION_IS_DTLS(s);

+    /* See if we support any EC or FFDHE ciphersuites */
     cipher_stack = SSL_get1_supported_ciphers(ssl);
     end = sk_SSL_CIPHER_num(cipher_stack);
     for (i = 0; i < end; i++) {
         const SSL_CIPHER *c = sk_SSL_CIPHER_value(cipher_stack, i);
-
-        alg_k = c->algorithm_mkey;
-        alg_a = c->algorithm_auth;
-        if ((alg_k & (SSL_kECDHE | SSL_kECDHEPSK))
-            || (alg_a & SSL_aECDSA)
-            || c->min_tls >= TLS1_3_VERSION) {
+        unsigned long alg_k = c->algorithm_mkey;
+        unsigned long alg_a = c->algorithm_auth;
+
+        int is_ffdhe_ciphersuite = (alg_k & (SSL_kDHE | SSL_kDHEPSK));
+        int is_ec_ciphersuite = ((alg_k & (SSL_kECDHE | SSL_kECDHEPSK))
+            || (alg_a & SSL_aECDSA));
+        int is_tls13 = (dtls ? DTLS_VERSION_GT(c->min_dtls, DTLS1_2_VERSION)
+                             : (c->min_tls > TLS1_2_VERSION));
+
+        if ((check_type == ffdhe_check && (is_ffdhe_ciphersuite || is_tls13))
+            || (check_type == ecdhe_check && (is_ec_ciphersuite || is_tls13))
+            || (check_type == ptfmt_check && is_ec_ciphersuite)) {
             ret = 1;
             break;
         }
     }
     sk_SSL_CIPHER_free(cipher_stack);
-    if (!ret)
+    if (ret == 0)
         return 0;

-    /* Check we have at least one EC supported group */
+    /* Check we have at least one EC or FFDHE supported group */
     tls1_get_supported_groups(s, &pgroups, &num_groups);
     for (j = 0; j < num_groups; j++) {
         uint16_t ctmp = pgroups[j];
+        const TLS_GROUP_INFO *ginfo = NULL;
+
+        if (!tls_valid_group(s, ctmp, min_version, max_version, NULL, &ginfo))
+            continue;

-        if (tls_valid_group(s, ctmp, min_version, max_version, 1, NULL)
+        if (check_type == ffdhe_check && is_ffdhe_group(ginfo->group_id)
             && tls_group_allowed(s, ctmp, SSL_SECOP_CURVE_SUPPORTED))
             return 1;
-    }

+        if (check_type != ffdhe_check && is_ecdhe_group(ginfo->group_id)
+            && tls_group_allowed(s, ctmp, SSL_SECOP_CURVE_SUPPORTED))
+            return 1;
+    }
     return 0;
 }

@@ -189,12 +232,14 @@ EXT_RETURN tls_construct_ctos_ec_pt_formats(SSL_CONNECTION *s, WPACKET *pkt,
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, reason);
         return EXT_RETURN_FAIL;
     }
-    if (!use_ecc(s, min_version, max_version))
+    if (!negotiate_dhe(s, ptfmt_check, min_version, max_version))
         return EXT_RETURN_NOT_SENT;

-    /* Add TLS extension ECPointFormats to the ClientHello message */
     tls1_get_formatlist(s, &pformats, &num_formats);
+    if (num_formats == 0)
+        return EXT_RETURN_NOT_SENT;

+    /* Add TLS extension ECPointFormats to the ClientHello message */
     if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ec_point_formats)
         /* Sub-packet for formats extension */
         || !WPACKET_start_sub_packet_u16(pkt)
@@ -214,6 +259,8 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
     const uint16_t *pgroups = NULL;
     size_t num_groups = 0, i, tls13added = 0, added = 0;
     int min_version, max_version, reason;
+    int dtls = SSL_CONNECTION_IS_DTLS(s);
+    int use_ecdhe, use_ffdhe;

     reason = ssl_get_min_max_version(s, &min_version, &max_version, NULL);
     if (reason != 0) {
@@ -222,11 +269,13 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
     }

     /*
-     * We only support EC groups in TLSv1.2 or below, and in DTLS. Therefore
-     * if we don't have EC support then we don't send this extension.
+     * If we don't support suitable groups, don't send the extension
      */
-    if (!use_ecc(s, min_version, max_version)
-        && (SSL_CONNECTION_IS_DTLS(s) || max_version < TLS1_3_VERSION))
+    use_ecdhe = negotiate_dhe(s, ecdhe_check, min_version, max_version);
+    use_ffdhe = negotiate_dhe(s, ffdhe_check, min_version, max_version);
+    if (!use_ecdhe && !use_ffdhe
+        && (dtls ? DTLS_VERSION_LE(max_version, DTLS1_2_VERSION)
+                 : (max_version <= TLS1_2_VERSION)))
         return EXT_RETURN_NOT_SENT;

     /*
@@ -244,19 +293,25 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
     }
     /* Copy group ID if supported */
     for (i = 0; i < num_groups; i++) {
+        const TLS_GROUP_INFO *ginfo = NULL;
         uint16_t ctmp = pgroups[i];
         int okfortls13;

-        if (tls_valid_group(s, ctmp, min_version, max_version, 0, &okfortls13)
-            && tls_group_allowed(s, ctmp, SSL_SECOP_CURVE_SUPPORTED)) {
-            if (!WPACKET_put_bytes_u16(pkt, ctmp)) {
-                SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-                return EXT_RETURN_FAIL;
-            }
-            if (okfortls13 && max_version == TLS1_3_VERSION)
-                tls13added++;
-            added++;
+        if (!tls_valid_group(s, ctmp, min_version, max_version, &okfortls13,
+                &ginfo)
+            || (!use_ecdhe && is_ecdhe_group(ginfo->group_id))
+            || (!use_ffdhe && is_ffdhe_group(ginfo->group_id))
+            /* Note: SSL_SECOP_CURVE_SUPPORTED covers all key exchange groups */
+            || !tls_group_allowed(s, ctmp, SSL_SECOP_CURVE_SUPPORTED))
+            continue;
+
+        if (!WPACKET_put_bytes_u16(pkt, ctmp)) {
+            SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+            return EXT_RETURN_FAIL;
         }
+        if (okfortls13 && max_version == TLS1_3_VERSION)
+            tls13added++;
+        added++;
     }
     if (!WPACKET_close(pkt) || !WPACKET_close(pkt)) {
         if (added == 0)
@@ -746,7 +801,7 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt,
             if (!tls_group_allowed(s, pgroups[i], SSL_SECOP_CURVE_SUPPORTED))
                 continue;
             if (!tls_valid_group(s, pgroups[i], TLS1_3_VERSION, TLS1_3_VERSION,
-                    0, NULL))
+                    NULL, NULL))
                 continue;

             group_id = pgroups[i];
@@ -1912,7 +1967,7 @@ int tls_parse_stoc_key_share(SSL_CONNECTION *s, PACKET *pkt,
         if (i >= num_groups
             || !tls_group_allowed(s, group_id, SSL_SECOP_CURVE_SUPPORTED)
             || !tls_valid_group(s, group_id, TLS1_3_VERSION, TLS1_3_VERSION,
-                0, NULL)) {
+                NULL, NULL)) {
             SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_BAD_KEY_SHARE);
             return 0;
         }
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index f241861740..a0ac7653f1 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -731,7 +731,7 @@ static KS_EXTRACTION_RESULT extract_keyshares(SSL_CONNECTION *s, PACKET *key_sha
         if (!check_in_list(s, group_id, srvrgroups, srvr_num_groups, 1, NULL)
             || !tls_group_allowed(s, group_id, SSL_SECOP_CURVE_SUPPORTED)
             || !tls_valid_group(s, group_id, TLS1_3_VERSION, TLS1_3_VERSION,
-                0, NULL)) {
+                NULL, NULL)) {
             /* Share not suitable or not supported, check next share */
             continue;
         }
@@ -807,7 +807,7 @@ static void check_overlap(SSL_CONNECTION *s,
             || !tls_group_allowed(s, candidate_groups[current_group],
                 SSL_SECOP_CURVE_SUPPORTED)
             || !tls_valid_group(s, candidate_groups[current_group], TLS1_3_VERSION,
-                TLS1_3_VERSION, 0, NULL))
+                TLS1_3_VERSION, NULL, NULL))
             /* No overlap or group not suitable, check next group */
             continue;

@@ -1692,7 +1692,7 @@ EXT_RETURN tls_construct_stoc_supported_groups(SSL_CONNECTION *s, WPACKET *pkt,
     for (i = 0; i < numgroups; i++) {
         uint16_t group = groups[i];

-        if (tls_valid_group(s, group, version, version, 0, NULL)
+        if (tls_valid_group(s, group, version, version, NULL, NULL)
             && tls_group_allowed(s, group, SSL_SECOP_CURVE_SUPPORTED)) {
             if (first) {
                 /*
diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c
index ea011c71a7..2f6cef3343 100644
--- a/ssl/statem/statem_srvr.c
+++ b/ssl/statem/statem_srvr.c
@@ -2629,8 +2629,8 @@ CON_FUNC_RETURN tls_construct_server_key_exchange(SSL_CONNECTION *s,
                 }
 #if !defined(OPENSSL_NO_DEPRECATED_3_0)
                 if ((pkdhp == NULL) && (s->cert->dh_tmp_cb != NULL)) {
-                    pkdh = ssl_dh_to_pkey(s->cert->dh_tmp_cb(SSL_CONNECTION_GET_USER_SSL(s),
-                        0, 1024));
+                    pkdh = ssl_dh_to_pkey(
+                        s->cert->dh_tmp_cb(SSL_CONNECTION_GET_USER_SSL(s), 0, 1024));
                     if (pkdh == NULL) {
                         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
                         goto err;
diff --git a/ssl/t1_lib.c b/ssl/t1_lib.c
index b0bfc92e32..aca0bc6398 100644
--- a/ssl/t1_lib.c
+++ b/ssl/t1_lib.c
@@ -857,25 +857,25 @@ void tls1_get_group_tuples(SSL_CONNECTION *s, const size_t **ptuples,
 }

 int tls_valid_group(SSL_CONNECTION *s, uint16_t group_id,
-    int minversion, int maxversion,
-    int isec, int *okfortls13)
+    int minversion, int maxversion, int *okfortls13,
+    const TLS_GROUP_INFO **giptr)
 {
     const TLS_GROUP_INFO *ginfo = tls1_group_id_lookup(SSL_CONNECTION_GET_CTX(s),
         group_id);
-    int ret;
+    int ret = 0;
     int group_minversion, group_maxversion;

     if (okfortls13 != NULL)
         *okfortls13 = 0;

     if (ginfo == NULL)
-        return 0;
+        goto end;

     group_minversion = SSL_CONNECTION_IS_DTLS(s) ? ginfo->mindtls : ginfo->mintls;
     group_maxversion = SSL_CONNECTION_IS_DTLS(s) ? ginfo->maxdtls : ginfo->maxtls;

     if (group_minversion < 0 || group_maxversion < 0)
-        return 0;
+        goto end;
     if (group_maxversion == 0)
         ret = 1;
     else
@@ -888,11 +888,9 @@ int tls_valid_group(SSL_CONNECTION *s, uint16_t group_id,
             *okfortls13 = (group_maxversion == 0)
                 || (group_maxversion >= TLS1_3_VERSION);
     }
-    ret &= !isec
-        || strcmp(ginfo->algorithm, "EC") == 0
-        || strcmp(ginfo->algorithm, "X25519") == 0
-        || strcmp(ginfo->algorithm, "X448") == 0;
-
+end:
+    if (giptr != NULL)
+        *giptr = ginfo;
     return ret;
 }

diff --git a/test/recipes/70-test_sslmessages.t b/test/recipes/70-test_sslmessages.t
index bb1789710c..7871e349cd 100644
--- a/test/recipes/70-test_sslmessages.t
+++ b/test/recipes/70-test_sslmessages.t
@@ -100,7 +100,7 @@ my $proxy = TLSProxy::Proxy->new(
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_STATUS_REQUEST,
         TLSProxy::Message::CLIENT,
         checkhandshake::STATUS_REQUEST_CLI_EXTENSION],
-    (disabled("ec") ? () :
+    ((disabled("ec") && disabled("dh")) ? () :
                       [TLSProxy::Message::MT_CLIENT_HELLO,
                        TLSProxy::Message::EXT_SUPPORTED_GROUPS,
                        TLSProxy::Message::CLIENT,
diff --git a/test/recipes/70-test_tls13kexmodes.t b/test/recipes/70-test_tls13kexmodes.t
index cd71a313b8..aba24ec93a 100644
--- a/test/recipes/70-test_tls13kexmodes.t
+++ b/test/recipes/70-test_tls13kexmodes.t
@@ -70,9 +70,10 @@ plan skip_all => "$test_name needs EC enabled"
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SUPPORTED_GROUPS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
-    [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
-        TLSProxy::Message::CLIENT,
-        checkhandshake::DEFAULT_EXTENSIONS],
+    (disabled("tls1_2") ? () :
+        [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
+            TLSProxy::Message::CLIENT,
+            checkhandshake::DEFAULT_EXTENSIONS]),
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SIG_ALGS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
@@ -123,9 +124,10 @@ plan skip_all => "$test_name needs EC enabled"
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SUPPORTED_GROUPS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
-    [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
-        TLSProxy::Message::CLIENT,
-        checkhandshake::DEFAULT_EXTENSIONS],
+    (disabled("tls1_2") ? () :
+        [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
+            TLSProxy::Message::CLIENT,
+            checkhandshake::DEFAULT_EXTENSIONS]),
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SIG_ALGS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
diff --git a/test/recipes/70-test_tls13messages.t b/test/recipes/70-test_tls13messages.t
index 3a04cca334..af223b56a8 100644
--- a/test/recipes/70-test_tls13messages.t
+++ b/test/recipes/70-test_tls13messages.t
@@ -70,9 +70,10 @@ plan skip_all => "$test_name needs EC enabled"
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SUPPORTED_GROUPS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
-    [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
-        TLSProxy::Message::CLIENT,
-        checkhandshake::DEFAULT_EXTENSIONS],
+    (disabled("tls1_2") ? () :
+        [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
+            TLSProxy::Message::CLIENT,
+            checkhandshake::DEFAULT_EXTENSIONS]),
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SIG_ALGS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
@@ -126,9 +127,10 @@ plan skip_all => "$test_name needs EC enabled"
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SUPPORTED_GROUPS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
-    [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
-        TLSProxy::Message::CLIENT,
-        checkhandshake::DEFAULT_EXTENSIONS],
+    (disabled("tls1_2") ? () :
+        [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_EC_POINT_FORMATS,
+            TLSProxy::Message::CLIENT,
+            checkhandshake::DEFAULT_EXTENSIONS]),
     [TLSProxy::Message::MT_CLIENT_HELLO, TLSProxy::Message::EXT_SIG_ALGS,
         TLSProxy::Message::CLIENT,
         checkhandshake::DEFAULT_EXTENSIONS],
diff --git a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
index 535b37b2bd..f797cac128 100644
--- a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
+++ b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt
@@ -19,8 +19,6 @@ Header:
           000f - 01 02 04 04 80 0c 00 00-05 04 80 08 00 00 06   ...............
           001e - 04 80 08 00 00 07 04 80-08 00 00 08 02 40 64   .............@d
           002d - 09 02 40 64                                    ..@d
-        extension_type=ec_point_formats(11), length=2
-          uncompressed (0)
         extension_type=supported_groups(10), length=18
           X25519MLKEM768 (4588)
           ecdh_x25519 (29)
diff --git a/test/recipes/75-test_quicapi_data/ssltraceref.txt b/test/recipes/75-test_quicapi_data/ssltraceref.txt
index c5502b68a7..fe5a32bea3 100644
--- a/test/recipes/75-test_quicapi_data/ssltraceref.txt
+++ b/test/recipes/75-test_quicapi_data/ssltraceref.txt
@@ -19,8 +19,6 @@ Header:
           000f - 01 02 04 04 80 0c 00 00-05 04 80 08 00 00 06   ...............
           001e - 04 80 08 00 00 07 04 80-08 00 00 08 02 40 64   .............@d
           002d - 09 02 40 64                                    ..@d
-        extension_type=ec_point_formats(11), length=2
-          uncompressed (0)
         extension_type=supported_groups(10), length=18
           X25519MLKEM768 (4588)
           ecdh_x25519 (29)
diff --git a/test/recipes/80-test_ssl_old.t b/test/recipes/80-test_ssl_old.t
index 0039fe2172..a0653ef216 100644
--- a/test/recipes/80-test_ssl_old.t
+++ b/test/recipes/80-test_ssl_old.t
@@ -566,11 +566,16 @@ sub testssl {

           SKIP: {
               skip "skipping dhe512 test", 1
-                  if ($no_dh);
+                  if ($no_dh || $no_ec);

+              # Need some explicit EC groups to suppress default support of
+              # ffdhe2048 and ffdhe3072 in the client hello, which then
+              # overrides the server's DH temp parameters from "-dh512".
+              #
               is(run(test([@ssltest,
                            "-s_cipher", "EDH",
                            "-c_cipher", 'EDH:@SECLEVEL=1',
+                           "-groups", "?P-256:?X25519:?MLKEM512",
                            "-dhe512",
                            $protocol])), 0,
                  "testing connection with weak DH, expecting failure");
diff --git a/test/recipes/90-test_sslapi_data/ssltraceref-zlib.txt b/test/recipes/90-test_sslapi_data/ssltraceref-zlib.txt
index 4c58d23db5..a9a503345f 100644
--- a/test/recipes/90-test_sslapi_data/ssltraceref-zlib.txt
+++ b/test/recipes/90-test_sslapi_data/ssltraceref-zlib.txt
@@ -14,10 +14,6 @@ Header:
       compression_methods (len=1)
         No Compression (0x00)
       extensions, length = ?
-        extension_type=ec_point_formats(11), length=4
-          uncompressed (0)
-          ansiX962_compressed_prime (1)
-          ansiX962_compressed_char2 (2)
         extension_type=supported_groups(10), length=20
           MLKEM512 (512)
           MLKEM768 (513)
diff --git a/test/recipes/90-test_sslapi_data/ssltraceref.txt b/test/recipes/90-test_sslapi_data/ssltraceref.txt
index 451c98284b..273fd35c11 100644
--- a/test/recipes/90-test_sslapi_data/ssltraceref.txt
+++ b/test/recipes/90-test_sslapi_data/ssltraceref.txt
@@ -14,10 +14,6 @@ Header:
       compression_methods (len=1)
         No Compression (0x00)
       extensions, length = ?
-        extension_type=ec_point_formats(11), length=4
-          uncompressed (0)
-          ansiX962_compressed_prime (1)
-          ansiX962_compressed_char2 (2)
         extension_type=supported_groups(10), length=20
           MLKEM512 (512)
           MLKEM768 (513)
diff --git a/test/ssl_old_test.c b/test/ssl_old_test.c
index ea3c204026..49e5a954d2 100644
--- a/test/ssl_old_test.c
+++ b/test/ssl_old_test.c
@@ -648,6 +648,9 @@ static void sv_usage(void)
     fprintf(stderr,
         " -dhe4096      - use 4096 bit key (safe prime) for DHE\n");
 #endif
+    fprintf(
+        stderr,
+        " -groups <list> - override the default client supported groups list\n");
     fprintf(stderr, " -no_dhe       - disable DHE\n");
 #ifndef OPENSSL_NO_EC
     fprintf(stderr, " -no_ecdhe     - disable ECDHE\n");
@@ -910,9 +913,10 @@ int main(int argc, char *argv[])
     long bytes = 256L;
 #ifndef OPENSSL_NO_DH
     EVP_PKEY *dhpkey;
-    int dhe512 = 0, dhe1024dsa = 0, dhe4096 = 0;
+    int dhe512 = 0, dhe1024dsa = 0, dhe2048 = 0, dhe4096 = 0;
     int no_dhe = 0;
 #endif
+    const char *groups = NULL;
     int no_psk = 0;
     int print_time = 0;
     clock_t s_time = 0, c_time = 0;
@@ -1001,6 +1005,8 @@ int main(int argc, char *argv[])
             dhe512 = 1;
         else if (strcmp(*argv, "-dhe1024dsa") == 0)
             dhe1024dsa = 1;
+        else if (strcmp(*argv, "-dhe2048") == 0)
+            dhe2048 = 1;
         else if (strcmp(*argv, "-dhe4096") == 0)
             dhe4096 = 1;
 #endif
@@ -1018,6 +1024,10 @@ int main(int argc, char *argv[])
 #else
             no_psk = 1;
 #endif
+        } else if (strcmp(*argv, "-groups") == 0) {
+            if (--argc < 1)
+                goto bad;
+            groups = *(++argv);
         } else if (strcmp(*argv, "-tls1_2") == 0) {
             tls1_2 = 1;
         } else if (strcmp(*argv, "-tls1_1") == 0) {
@@ -1502,6 +1512,8 @@ int main(int argc, char *argv[])
             dhpkey = get_dh1024dsa(libctx);
         else if (dhe512)
             dhpkey = get_dh512(libctx);
+        else if (dhe2048)
+            dhpkey = get_dh2048(libctx);
         else if (dhe4096)
             dhpkey = get_dh4096(libctx);
         else
@@ -1519,6 +1531,12 @@ int main(int argc, char *argv[])
             EVP_PKEY_free(dhpkey);
     }
 #endif
+    if (groups != NULL && !SSL_CTX_set1_groups_list(c_ctx, groups)) {
+        BIO_printf(bio_err, "error setting client supported groups to: %s\n",
+            groups);
+        ERR_print_errors(bio_err);
+        goto end;
+    }

     if (!(SSL_CTX_load_verify_file(s_ctx, CAfile)
             || SSL_CTX_load_verify_dir(s_ctx, CApath))
diff --git a/test/sslapitest.c b/test/sslapitest.c
index 5e2ac8daec..eb57864979 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -5389,6 +5389,7 @@ static int test_key_exchange(int idx)
     char *kexch_name0 = NULL;
     const char *kexch_names = NULL;
     int shared_group0;
+    const char *client_group_name = NULL;

     switch (idx) {
 #ifndef OPENSSL_NO_EC
@@ -5433,7 +5434,6 @@ static int test_key_exchange(int idx)
 #ifndef OPENSSL_NO_TLS1_2
     case 20:
         max_version = TLS1_2_VERSION;
-        kexch_name0 = "ffdhe2048";
 #endif
         /* Fall through */
     case 6:
@@ -5466,17 +5466,21 @@ static int test_key_exchange(int idx)
 #if !defined(OPENSSL_NO_TLS1_2)
     case 19:
         max_version = TLS1_2_VERSION;
+        kexch_groups = NULL;
 #if !defined(OPENSSL_NO_EC)
         /* Set at least one EC group so the handshake completes */
         kexch_names = "MLKEM512:MLKEM768:MLKEM1024:secp256r1";
+        kexch_name0 = "secp256r1";
+        break;
 #elif !defined(OPENSSL_NO_DH)
-        kexch_names = "MLKEM512:MLKEM768:MLKEM1024";
+        kexch_names = "MLKEM512:MLKEM768:MLKEM1024:ffdhe2048";
+        kexch_name0 = "ffdhe2048";
+        break;
 #else
         /* With neither EC nor DH TLS 1.2 can't happen */
         return 1;
 #endif
 #endif
-        /* Fall through */
     case 12:
         kexch_groups = NULL;
         if (kexch_names == NULL)
@@ -5573,42 +5577,28 @@ static int test_key_exchange(int idx)
     if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
         goto end;

+    shared_group0 = SSL_get_shared_group(serverssl, 0);
+    if (kexch_groups != NULL
+        && !TEST_int_eq(shared_group0, kexch_groups[0]))
+        goto end;
+    if (!TEST_str_eq(SSL_group_to_name(serverssl, shared_group0),
+            kexch_name0))
+        goto end;
     /*
-     * If the handshake succeeds the negotiated kexch alg should be the first
-     * one in configured, except in the case of "all" FFDHE and "all" ML-KEM
-     * groups (idx == 19, 20), which are TLSv1.3 only so we expect no shared
-     * group to exist.
+     * With TLS <= 1.2, the client will not infer an FFDHE group name from
+     * the server's DH parameters, so we allow NULL client names in that
+     * case.
      */
-    shared_group0 = SSL_get_shared_group(serverssl, 0);
-    switch (idx) {
-    case 19:
-#if !defined(OPENSSL_NO_EC)
-        /* MLKEM + TLS 1.2 and no DH => "secp526r1" */
-        if (!TEST_int_eq(shared_group0, NID_X9_62_prime256v1))
-            goto end;
-        break;
-#endif
-        /* Fall through */
-    case 20:
-        if (!TEST_int_eq(shared_group0, 0))
-            goto end;
-        break;
-    default:
-        if (kexch_groups != NULL
-            && !TEST_int_eq(shared_group0, kexch_groups[0]))
-            goto end;
-        if (!TEST_str_eq(SSL_group_to_name(serverssl, shared_group0),
-                kexch_name0))
-            goto end;
-        if (!TEST_str_eq(SSL_get0_group_name(serverssl), kexch_name0)
-            || !TEST_str_eq(SSL_get0_group_name(clientssl), kexch_name0))
-            goto end;
-        if (!TEST_int_eq(SSL_get_negotiated_group(serverssl), shared_group0))
-            goto end;
-        if (!TEST_int_eq(SSL_get_negotiated_group(clientssl), shared_group0))
-            goto end;
-        break;
-    }
+    client_group_name = SSL_get0_group_name(clientssl);
+    if (!TEST_str_eq(SSL_get0_group_name(serverssl), kexch_name0)
+        || ((max_version >= TLS1_3_VERSION || client_group_name != NULL)
+            && !TEST_str_eq(client_group_name, kexch_name0)))
+        goto end;
+    if (!TEST_int_eq(SSL_get_negotiated_group(serverssl), shared_group0))
+        goto end;
+    if (client_group_name != NULL
+        && !TEST_int_eq(SSL_get_negotiated_group(clientssl), shared_group0))
+        goto end;

     testresult = 1;
 end:
@@ -5713,11 +5703,7 @@ static int test_negotiated_group(int idx)
         kexch_alg = ecdhe_kexch_groups[idx];
     else
         kexch_alg = ffdhe_kexch_groups[idx];
-    /* We expect nothing for the unimplemented TLS 1.2 FFDHE named groups */
-    if (!istls13 && !isecdhe)
-        expectednid = NID_undef;
-    else
-        expectednid = kexch_alg;
+    expectednid = kexch_alg;

     if (is_fips && (kexch_alg == NID_X25519 || kexch_alg == NID_X448))
         return TEST_skip("X25519 and X448 might not be available in fips provider.");
@@ -5754,9 +5740,13 @@ static int test_negotiated_group(int idx)
     if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
         goto end;

-    /* Initial handshake; always the configured one */
-    if (!TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)
-        || !TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid))
+    /*
+     * Initial handshake; always the configured one.  With TLS <= 1.2 and FFDHE
+     * the client does not infer a negotiated group id.
+     */
+    if (!TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid)
+        || ((istls13 || isecdhe)
+            && !TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)))
         goto end;

     if (!TEST_ptr((origsess = SSL_get1_session(clientssl))))
@@ -5781,8 +5771,9 @@ static int test_negotiated_group(int idx)
         goto end;

     /* Still had better agree, since nothing changed... */
-    if (!TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)
-        || !TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid))
+    if (!TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid)
+        || ((istls13 || isecdhe)
+            && !TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)))
         goto end;

     SSL_shutdown(clientssl);
@@ -5809,11 +5800,7 @@ static int test_negotiated_group(int idx)
         if (!TEST_int_ne(expectednid, kexch_alg))
             goto end;
     } else {
-        /* TLS 1.2 only supports named groups for ECDHE. */
-        if (isecdhe)
-            expectednid = kexch_alg;
-        else
-            expectednid = 0;
+        expectednid = kexch_alg;
     }
     if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
             NULL, NULL))
@@ -5827,8 +5814,9 @@ static int test_negotiated_group(int idx)
         goto end;

     /* Check that we get what we expected */
-    if (!TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)
-        || !TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid))
+    if (!TEST_uint_eq(SSL_get_negotiated_group(serverssl), expectednid)
+        || ((istls13 || isecdhe)
+            && !TEST_uint_eq(SSL_get_negotiated_group(clientssl), expectednid)))
         goto end;

     testresult = 1;
@@ -11447,7 +11435,6 @@ end:
     return testresult;
 }

-#ifndef OPENSSL_NO_TLS1_3
 /*
  * Test the server will reject FFDHE ciphersuites if no supported FFDHE group is
  * advertised by the client.
@@ -11667,7 +11654,6 @@ end:

     return testresult;
 }
-#endif /* OPENSSL_NO_TLS1_3 */

 #endif /* OPENSSL_NO_DH */
 #endif /* OPENSSL_NO_TLS1_2 */
@@ -14495,12 +14481,10 @@ int setup_tests(void)
 #ifndef OPENSSL_NO_DH
     ADD_ALL_TESTS(test_set_tmp_dh, 11);
     ADD_ALL_TESTS(test_dh_auto, 7);
-#ifndef OPENSSL_NO_TLS1_3
     ADD_ALL_TESTS(test_no_shared_ffdhe_group, 10);
     ADD_ALL_TESTS(test_shared_ffdhe_group, 5);
 #endif
 #endif
-#endif
 #ifndef OSSL_NO_USABLE_TLS1_3
     ADD_TEST(test_sni_tls13);
     ADD_ALL_TESTS(test_ticket_lifetime, 2);