Support equipreference groups in QuicCryptoServerConfig

Adds a new config setter for preferred_groups in QuicCryptoServerConfig, and updates TlsServerHandshaker to call SSL_set1_group_ids_with_flags if there are equipreference groups. This allows the server to negotiate key exchange groups with equal preference, i.e. letting the client's preference decide which group among equally preferred groups is selected.

The analogous change on the client side is less meaningful, because a client's preferred_groups is always serialized on the wire as a strictly ordered list, and cannot express the concept of equipreference groups.

This functionality is needed to talk to CNSA2.0 compliant clients which prefer ML-KEM-1024 over X25519-MLKEM768, while still negotiating X25519-MLKEM768 for the vast majority of clients who don't specifically prefer ML-KEM-1024.

PiperOrigin-RevId: 1000822559
diff --git a/quiche/quic/core/crypto/quic_crypto_server_config.h b/quiche/quic/core/crypto/quic_crypto_server_config.h
index 5e9c78d..ebe2693 100644
--- a/quiche/quic/core/crypto/quic_crypto_server_config.h
+++ b/quiche/quic/core/crypto/quic_crypto_server_config.h
@@ -17,6 +17,7 @@
 #include "absl/strings/string_view.h"
 #include "absl/synchronization/mutex.h"
 #include "openssl/base.h"
+#include "openssl/ssl.h"
 #include "quiche/quic/core/crypto/crypto_handshake.h"
 #include "quiche/quic/core/crypto/crypto_handshake_message.h"
 #include "quiche/quic/core/crypto/crypto_protocol.h"
@@ -32,6 +33,7 @@
 #include "quiche/quic/core/quic_time.h"
 #include "quiche/quic/platform/api/quic_export.h"
 #include "quiche/quic/platform/api/quic_socket_address.h"
+#include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/platform/api/quiche_reference_counted.h"
 
 namespace quic {
@@ -447,11 +449,47 @@
     return preferred_groups_;
   }
 
+  // Flags associated with each group in |preferred_groups()|.
+  const std::vector<uint32_t>& preferred_groups_flags() const {
+    return preferred_groups_flags_;
+  }
+
+  // Returns whether the preferred groups list specified by |preferred_groups()|
+  // and |preferred_groups_flags()| has equipreference groups.
+  bool preferred_groups_has_equipreference() const {
+    return preferred_groups_has_equipreference_;
+  }
+
   // Sets the preferred groups that will be used in the TLS handshake. Values
   // in the |preferred_groups| vector are NamedGroup enum codepoints from
   // https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.7.
+  // This sets the preferred groups list to each of the given groups in strictly
+  // decreasing preference order. If equipreference groups are desired, use
+  // |append_equally_preferred_groups| instead.
   void set_preferred_groups(const std::vector<uint16_t>& preferred_groups) {
     preferred_groups_ = preferred_groups;
+    preferred_groups_flags_.clear();
+    preferred_groups_flags_.resize(preferred_groups_.size(), 0);
+    preferred_groups_has_equipreference_ = false;
+  }
+
+  // Appends the given groups to the end of the preferred groups list, with
+  // equal preference to each other.
+  void append_equally_preferred_groups(
+      const std::vector<uint16_t>& equally_preferred_groups) {
+    if (equally_preferred_groups.empty()) {
+      return;
+    }
+    preferred_groups_.insert(preferred_groups_.end(),
+                             equally_preferred_groups.begin(),
+                             equally_preferred_groups.end());
+    preferred_groups_has_equipreference_ = true;
+    for (size_t i = 0; i < equally_preferred_groups.size() - 1; ++i) {
+      preferred_groups_flags_.push_back(
+          SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT);
+    }
+    preferred_groups_flags_.push_back(0);
+    QUICHE_DCHECK(preferred_groups_.size() == preferred_groups_flags_.size());
   }
 
   // Pre-shared key used during the handshake.
@@ -916,6 +954,18 @@
 
   // The groups to use for key exchange in the TLS handshake;
   std::vector<uint16_t> preferred_groups_;
+  // Flags associated with each group in |preferred_groups_|. Each element is a
+  // bit set consisting of |SSL_GROUP_FLAG_*| values OR'ed together.
+  // Invariant: |preferred_groups_flags_| is always the same size as
+  // |preferred_groups_|.
+  std::vector<uint32_t> preferred_groups_flags_;
+  // Whether the preferred groups list specified by |preferred_groups_| and
+  // |preferred_groups_flags_| may contain groups with equal preference. This is
+  // only true if |append_equally_preferred_groups| was called.
+  // This boolean is always true when |preferred_groups_flags_|
+  // contains any non-zero values, though it may also be true when all the
+  // flags are zero.
+  bool preferred_groups_has_equipreference_ = false;
 
   // These fields store configuration values. See the comments for their
   // respective setter functions.
diff --git a/quiche/quic/core/tls_server_handshaker.cc b/quiche/quic/core/tls_server_handshaker.cc
index cc8dd85..759a398 100644
--- a/quiche/quic/core/tls_server_handshaker.cc
+++ b/quiche/quic/core/tls_server_handshaker.cc
@@ -247,8 +247,21 @@
     tls_connection_.EnableInfoCallback();
   }
   if (!crypto_config->preferred_groups().empty()) {
-    SSL_set1_group_ids(ssl(), crypto_config->preferred_groups().data(),
-                       crypto_config->preferred_groups().size());
+    QUICHE_DCHECK(crypto_config->preferred_groups().size() ==
+                  crypto_config->preferred_groups_flags().size());
+    // If preferred_groups_flags() contains all zeros, then
+    // SSL_set1_group_ids_with_flags() and SSL_set1_group_ids() are equivalent.
+    // TODO(chlily): Simplify this code by always calling
+    // SSL_set1_group_ids_with_flags().
+    if (crypto_config->preferred_groups_has_equipreference()) {
+      SSL_set1_group_ids_with_flags(
+          ssl(), crypto_config->preferred_groups().data(),
+          crypto_config->preferred_groups_flags().data(),
+          crypto_config->preferred_groups().size());
+    } else {
+      SSL_set1_group_ids(ssl(), crypto_config->preferred_groups().data(),
+                         crypto_config->preferred_groups().size());
+    }
   }
 
 #if BORINGSSL_API_VERSION >= 41
diff --git a/quiche/quic/core/tls_server_handshaker_test.cc b/quiche/quic/core/tls_server_handshaker_test.cc
index 7ec14a1..0985465 100644
--- a/quiche/quic/core/tls_server_handshaker_test.cc
+++ b/quiche/quic/core/tls_server_handshaker_test.cc
@@ -1646,6 +1646,134 @@
             SSL_get_group_id(server_stream()->GetSsl()));
 }
 
+TEST_P(TlsServerHandshakerTest, SetPreferredGroups) {
+  // The server has a strictly ordered list of preferred groups. The negotiated
+  // group is the first one in the list that is supported by the client.
+  server_crypto_config_->set_preferred_groups(
+      {SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024, SSL_GROUP_X25519});
+
+  EXPECT_THAT(server_crypto_config_->preferred_groups(),
+              ElementsAre(SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024,
+                          SSL_GROUP_X25519));
+  EXPECT_THAT(server_crypto_config_->preferred_groups_flags(),
+              ElementsAre(0, 0, 0));
+  EXPECT_FALSE(server_crypto_config_->preferred_groups_has_equipreference());
+
+  client_crypto_config_->set_preferred_groups(
+      {SSL_GROUP_X25519, SSL_GROUP_MLKEM1024, SSL_GROUP_X25519_MLKEM768});
+
+  InitializeServer();
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_TRUE(server_stream()->version().IsIetfQuic());
+  EXPECT_EQ(SSL_GROUP_X25519_MLKEM768,
+            SSL_get_group_id(server_stream()->GetSsl()));
+}
+
+TEST_P(TlsServerHandshakerTest, AppendEquallyPreferredGroupsStrictlyOrdered) {
+  // The server's preferred groups are equivalent to the test case above, but
+  // configured using append_equally_preferred_groups() with single-element
+  // equipreference groups.
+  server_crypto_config_->append_equally_preferred_groups(
+      {SSL_GROUP_X25519_MLKEM768});
+  server_crypto_config_->append_equally_preferred_groups({SSL_GROUP_MLKEM1024});
+  server_crypto_config_->append_equally_preferred_groups({SSL_GROUP_X25519});
+
+  EXPECT_THAT(server_crypto_config_->preferred_groups(),
+              ElementsAre(SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024,
+                          SSL_GROUP_X25519));
+  EXPECT_THAT(server_crypto_config_->preferred_groups_flags(),
+              ElementsAre(0, 0, 0));
+  // This is true because append_equally_preferred_groups() was called, even
+  // though all groups in the list are strictly ordered.
+  EXPECT_TRUE(server_crypto_config_->preferred_groups_has_equipreference());
+
+  client_crypto_config_->set_preferred_groups(
+      {SSL_GROUP_X25519, SSL_GROUP_MLKEM1024, SSL_GROUP_X25519_MLKEM768});
+
+  InitializeServer();
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_TRUE(server_stream()->version().IsIetfQuic());
+  EXPECT_EQ(SSL_GROUP_X25519_MLKEM768,
+            SSL_get_group_id(server_stream()->GetSsl()));
+}
+
+TEST_P(TlsServerHandshakerTest, AppendEquallyPreferredGroupsEquipreference) {
+  // The server has two equally preferred groups. Between them, the client's
+  // preference determines the one chosen.
+  server_crypto_config_->append_equally_preferred_groups(
+      {SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024});
+  server_crypto_config_->append_equally_preferred_groups({SSL_GROUP_X25519});
+
+  EXPECT_THAT(server_crypto_config_->preferred_groups(),
+              ElementsAre(SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024,
+                          SSL_GROUP_X25519));
+  EXPECT_THAT(server_crypto_config_->preferred_groups_flags(),
+              ElementsAre(SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT, 0, 0));
+  EXPECT_TRUE(server_crypto_config_->preferred_groups_has_equipreference());
+
+  struct {
+    std::vector<uint16_t> client_preferred_groups;
+    uint16_t expected_negotiated_group;
+  } kTestCases[] = {
+      {{SSL_GROUP_X25519, SSL_GROUP_MLKEM1024, SSL_GROUP_X25519_MLKEM768},
+       SSL_GROUP_MLKEM1024},
+      {{SSL_GROUP_X25519, SSL_GROUP_X25519_MLKEM768, SSL_GROUP_MLKEM1024},
+       SSL_GROUP_X25519_MLKEM768},
+  };
+  for (const auto& test : kTestCases) {
+    client_crypto_config_->set_preferred_groups(test.client_preferred_groups);
+    InitializeServer();
+    InitializeFakeClient();
+    CompleteCryptoHandshake();
+    ExpectHandshakeSuccessful();
+    EXPECT_TRUE(server_stream()->version().IsIetfQuic());
+    EXPECT_EQ(test.expected_negotiated_group,
+              SSL_get_group_id(server_stream()->GetSsl()));
+  }
+}
+
+TEST_P(TlsServerHandshakerTest, SetPreferredGroupsFollowedByEquipreference) {
+  // Test that mixing the two config setters works as expected.
+  server_crypto_config_->set_preferred_groups(
+      {SSL_GROUP_X25519_MLKEM768, SSL_GROUP_X25519});
+  server_crypto_config_->append_equally_preferred_groups(
+      {SSL_GROUP_SECP256R1, SSL_GROUP_SECP384R1});
+
+  EXPECT_THAT(server_crypto_config_->preferred_groups(),
+              ElementsAre(SSL_GROUP_X25519_MLKEM768, SSL_GROUP_X25519,
+                          SSL_GROUP_SECP256R1, SSL_GROUP_SECP384R1));
+  EXPECT_THAT(server_crypto_config_->preferred_groups_flags(),
+              ElementsAre(0, 0, SSL_GROUP_FLAG_EQUAL_PREFERENCE_WITH_NEXT, 0));
+  EXPECT_TRUE(server_crypto_config_->preferred_groups_has_equipreference());
+
+  struct {
+    std::vector<uint16_t> client_preferred_groups;
+    uint16_t expected_negotiated_group;
+  } kTestCases[] = {
+      {{SSL_GROUP_SECP384R1, SSL_GROUP_SECP256R1, SSL_GROUP_X25519_MLKEM768,
+        SSL_GROUP_X25519},
+       SSL_GROUP_X25519_MLKEM768},
+      {{SSL_GROUP_SECP384R1, SSL_GROUP_SECP256R1, SSL_GROUP_X25519},
+       SSL_GROUP_X25519},
+      {{SSL_GROUP_SECP384R1, SSL_GROUP_SECP256R1}, SSL_GROUP_SECP384R1},
+      {{SSL_GROUP_SECP256R1, SSL_GROUP_SECP384R1}, SSL_GROUP_SECP256R1},
+  };
+  for (const auto& test : kTestCases) {
+    client_crypto_config_->set_preferred_groups(test.client_preferred_groups);
+    InitializeServer();
+    InitializeFakeClient();
+    CompleteCryptoHandshake();
+    ExpectHandshakeSuccessful();
+    EXPECT_TRUE(server_stream()->version().IsIetfQuic());
+    EXPECT_EQ(test.expected_negotiated_group,
+              SSL_get_group_id(server_stream()->GetSsl()));
+  }
+}
+
 TEST_P(TlsServerHandshakerTest, AlpsUseNewCodepoint) {
   const struct {
     bool client_use_alps_new_codepoint;