Use SSLCipherPreferenceList for TLS 1.3 ciphers

This change is a refactor to use a SSLCipherPreferenceList to hold the
TLS 1.3 cipher preference list in SSL_CTX and SSL_CONFIG. In
SSL_CTX_new, it is populated with one of two default lists depending on
whether we have EVP hardware. The default lists are ordered lists with
equipreference groups layered on top (for servers). SSL_new copies the
list from the SSL_CTX.

This CL also rewrites the logic for TLS 1.3 cipher suite selection for
SSL compliance policies to express it in terms of
SSLCipherPreferenceList.

This paves the way for a future CL which will add a general purpose API
to configure the list.

Bug: 545123692
Change-Id: Id57d698c288f603225706cd53b00cc696a6a6964
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/101167
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: Lily Chen <chlily@google.com>
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index c0ec883..62d5a0b 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -83,14 +83,6 @@
   }
 }
 
-static bool ssl_add_tls13_cipher(CBB *cbb, uint16_t cipher_id,
-                                 ssl_compliance_policy_t policy) {
-  if (ssl_tls13_cipher_meets_policy(cipher_id, policy)) {
-    return CBB_add_u16(cbb, cipher_id);
-  }
-  return true;
-}
-
 static bool ssl_write_client_cipher_list(const SSL_HANDSHAKE *hs, CBB *out,
                                          ssl_client_hello_type_t type) {
   const SSLImpl *const ssl = hs->ssl;
@@ -108,35 +100,10 @@
     return false;
   }
 
-  // Add TLS 1.3 ciphers. Order ChaCha20-Poly1305 relative to AES-GCM based on
-  // hardware support.
+  // Add TLS 1.3 ciphers in configured preference order.
   if (hs->max_version >= TLS1_3_VERSION) {
-    static const uint16_t kCiphersNoAESHardware[] = {
-        SSL_CIPHER_CHACHA20_POLY1305_SHA256,
-        SSL_CIPHER_AES_128_GCM_SHA256,
-        SSL_CIPHER_AES_256_GCM_SHA384,
-    };
-    static const uint16_t kCiphersAESHardware[] = {
-        SSL_CIPHER_AES_128_GCM_SHA256,
-        SSL_CIPHER_AES_256_GCM_SHA384,
-        SSL_CIPHER_CHACHA20_POLY1305_SHA256,
-    };
-    static const uint16_t kCiphersCNSA[] = {
-        SSL_CIPHER_AES_256_GCM_SHA384,
-        SSL_CIPHER_AES_128_GCM_SHA256,
-        SSL_CIPHER_CHACHA20_POLY1305_SHA256,
-    };
-
-    const bssl::Span<const uint16_t> ciphers =
-        ssl->config->compliance_policy == ssl_compliance_policy_cnsa_202407
-            ? bssl::Span<const uint16_t>(kCiphersCNSA)
-            : (EVP_has_aes_hardware()
-                   ? bssl::Span<const uint16_t>(kCiphersAESHardware)
-                   : bssl::Span<const uint16_t>(kCiphersNoAESHardware));
-
-    for (auto cipher : ciphers) {
-      if (!ssl_add_tls13_cipher(&child, cipher,
-                                ssl->config->compliance_policy)) {
+    for (const SSL_CIPHER *cipher : hs->config->tls13_cipher_list.ciphers()) {
+      if (!CBB_add_u16(&child, SSL_CIPHER_get_protocol_id(cipher))) {
         return false;
       }
     }
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index 0b4f1a4..d47af74 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -127,95 +127,6 @@
   return true;
 }
 
-static UniquePtr<STACK_OF(SSL_CIPHER)> ssl_parse_client_cipher_list(
-    const SSL_CLIENT_HELLO *client_hello) {
-  CBS cipher_suites;
-  CBS_init(&cipher_suites, client_hello->cipher_suites,
-           client_hello->cipher_suites_len);
-
-  UniquePtr<STACK_OF(SSL_CIPHER)> sk(sk_SSL_CIPHER_new_null());
-  if (!sk) {
-    return nullptr;
-  }
-
-  while (CBS_len(&cipher_suites) > 0) {
-    uint16_t cipher_suite;
-
-    if (!CBS_get_u16(&cipher_suites, &cipher_suite)) {
-      OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_IN_RECEIVED_CIPHER_LIST);
-      return nullptr;
-    }
-
-    const SSL_CIPHER *c = SSL_get_cipher_by_value(cipher_suite);
-    if (c != nullptr && !sk_SSL_CIPHER_push(sk.get(), c)) {
-      return nullptr;
-    }
-  }
-
-  return sk;
-}
-
-static const SSL_CIPHER *choose_cipher(SSL_HANDSHAKE *hs,
-                                       const STACK_OF(SSL_CIPHER) *client_pref,
-                                       uint32_t mask_k, uint32_t mask_a) {
-  SSLImpl *const ssl = hs->ssl;
-  const STACK_OF(SSL_CIPHER) *prio, *allow;
-  // in_group_flags will either be empty, or will contain an array of bytes
-  // which indicate equal-preference groups in the `prio` stack. See the
-  // comment about `in_group_flags` in the `SSLCipherPreferenceList`
-  // struct.
-  Span<const bool> in_group_flags;
-  // best_index contains the index of the best matching cipher suite found so
-  // far, indexed into `allow`. If `best_index` is `SIZE_MAX`, no matching
-  // cipher suite has been found yet.
-  size_t best_index = SIZE_MAX;
-
-  const SSLCipherPreferenceList *server_pref =
-      hs->config->cipher_list ? hs->config->cipher_list.get()
-                              : ssl->ctx->cipher_list.get();
-  if (ssl->options & SSL_OP_CIPHER_SERVER_PREFERENCE) {
-    prio = server_pref->ciphers();
-    in_group_flags = server_pref->in_group_flags();
-    allow = client_pref;
-  } else {
-    prio = client_pref;
-    in_group_flags = Span<const bool>();
-    allow = server_pref->ciphers();
-  }
-
-  for (size_t i = 0; i < sk_SSL_CIPHER_num(prio); i++) {
-    const SSL_CIPHER *c = sk_SSL_CIPHER_value(prio, i);
-    const bool in_group = !in_group_flags.empty() && in_group_flags[i];
-
-    size_t cipher_index;
-    if (  // Check if the cipher is supported for the current version.
-        SSL_CIPHER_get_min_version(c) <= ssl_protocol_version(ssl) &&  //
-        ssl_protocol_version(ssl) <= SSL_CIPHER_get_max_version(c) &&  //
-        // Check the cipher is supported for the server configuration.
-        (c->algorithm_mkey & mask_k) &&  //
-        (c->algorithm_auth & mask_a) &&  //
-        // Check the cipher is in the `allow` list.
-        sk_SSL_CIPHER_find(allow, &cipher_index, c)) {
-      // Within a group, `allow`'s preference order applies.
-      if (best_index == SIZE_MAX || best_index > cipher_index) {
-        best_index = cipher_index;
-      }
-    }
-
-    // We are about to leave a (possibly singleton) group, but we found a match
-    // in it, so that's our answer.
-    if (!in_group && best_index != SIZE_MAX) {
-      return sk_SSL_CIPHER_value(allow, best_index);
-    }
-  }
-
-  // The final cipher suite must end a group, so, if we found a match, we must
-  // have returned early above.
-  assert(best_index == SIZE_MAX);
-  OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_CIPHER);
-  return nullptr;
-}
-
 struct TLS12ServerParams {
   bool ok() const { return cipher != nullptr; }
 
@@ -226,7 +137,7 @@
 static TLS12ServerParams choose_params(SSL_HANDSHAKE *hs,
                                        const SSLCredential *cred,
                                        Span<const uint8_t> allowed_cert_types,
-                                       const STACK_OF(SSL_CIPHER) *client_pref,
+                                       const CBS *client_cipher_list,
                                        bool has_ecdhe_group) {
   // Determine the usable cipher suites.
   uint32_t mask_k = 0, mask_a = 0;
@@ -279,7 +190,14 @@
   }
 
   TLS12ServerParams params;
-  params.cipher = choose_cipher(hs, client_pref, mask_k, mask_a);
+  const SSLCipherPreferenceList *server_cipher_pref =
+      hs->config->cipher_list ? hs->config->cipher_list.get()
+                              : hs->ssl->ctx->cipher_list.get();
+  bool prioritize_client_pref =
+      (hs->ssl->options & SSL_OP_CIPHER_SERVER_PREFERENCE) == 0;
+  params.cipher = server_cipher_pref->ChooseCipher(
+      client_cipher_list, prioritize_client_pref, ssl_protocol_version(hs->ssl),
+      mask_k, mask_a);
   if (params.cipher == nullptr ||
       (cred != nullptr &&
        !ssl_credential_matches_requested_issuers(hs, cred))) {
@@ -750,11 +668,9 @@
   // TODO(davidben): In the course of picking these, we also pick the ECDHE
   // group and signature algorithm. It would be tidier if we saved that decision
   // and avoided redoing it later.
-  UniquePtr<STACK_OF(SSL_CIPHER)> client_pref =
-      ssl_parse_client_cipher_list(&client_hello);
-  if (client_pref == nullptr) {
-    return ssl_hs_error;
-  }
+  CBS client_cipher_list;
+  CBS_init(&client_cipher_list, client_hello.cipher_suites,
+           client_hello.cipher_suites_len);
   Array<SSLCredential *> creds;
   if (!ssl_get_full_credential_list(hs, &creds)) {
     return ssl_hs_error;
@@ -770,12 +686,12 @@
   if (creds.empty()) {
     // The caller may have configured no credentials, but set a PSK callback.
     params = choose_params(hs, /*cred=*/nullptr, *allowed_cert_types,
-                           client_pref.get(), has_ecdhe_group);
+                           &client_cipher_list, has_ecdhe_group);
   } else {
     // Select the first credential which works.
     for (SSLCredential *cred : creds) {
       ERR_clear_error();
-      params = choose_params(hs, cred, *allowed_cert_types, client_pref.get(),
+      params = choose_params(hs, cred, *allowed_cert_types, &client_cipher_list,
                              has_ecdhe_group);
       if (params.ok()) {
         hs->credential = UpRef(cred);
diff --git a/ssl/internal.h b/ssl/internal.h
index 65bd0b3..60ee51b 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -294,9 +294,15 @@
   SSLCipherPreferenceList() = default;
   ~SSLCipherPreferenceList() = default;
 
-  // Initializes a list with the specified ciphers and flags.
+  // Initializes a list with the specified ciphers and flags. Calling Init on a
+  // previously initialized list discards the previous contents.
   bool Init(UniquePtr<STACK_OF(SSL_CIPHER)> ciphers,
             Array<bool> in_group_flags);
+  // Same as above, but takes a list of cipher protocol IDs.
+  bool Init(Span<const uint16_t> cipher_ids, Span<const bool> in_group_flags);
+
+  // Reset clears any contents previously set by `Init`.
+  void Reset();
 
   // Makes `this` a deep copy of another (already initialized) instance.
   bool CopyFrom(const SSLCipherPreferenceList &);
@@ -304,13 +310,33 @@
   // Removes `cipher` from the preference list.
   void Remove(const SSL_CIPHER *cipher);
 
+  // Contains returns whether a cipher whose protocol ID is `cipher_id` appears
+  // in the list.
+  bool Contains(uint16_t cipher_id) const;
+
+  // ChooseCipher implements the logic for a server to select the most-preferred
+  // cipher satisfying the constraints that is listed in both `*this` and the
+  // client's preference list in `client_cipher_list`, which contains an ordered
+  // list of 2-byte cipher suite protocol IDs. The returned cipher must be
+  // supported for the SSL protocol `version`, and must match the key exchange
+  // algorithm mask `mask_k` and the server authentication mask `mask_a`. If
+  // `prioritize_client_pref` is true, then the client's preference list is
+  // prioritized over the list in `*this`. Otherwise, the server's preference
+  // list (`*this`) is prioritized. This function returns a pointer to the
+  // most-preferred shared cipher, or nullptr if no shared cipher was found.
+  const SSL_CIPHER *ChooseCipher(const CBS *client_cipher_list,
+                                 bool prioritize_client_pref, uint16_t version,
+                                 uint32_t mask_k, uint32_t mask_a) const;
+
   size_t size() const { return sk_SSL_CIPHER_num(ciphers_.get()); }
 
   const STACK_OF(SSL_CIPHER) *ciphers() const { return ciphers_.get(); }
-  STACK_OF(SSL_CIPHER) *ciphers() { return ciphers_.get(); }
-
   Span<const bool> in_group_flags() const { return in_group_flags_; }
 
+  // TODO(crbug.com/550501994): Remove the non-const overload. This may result
+  // in callers mutating the internal state in an inconsistent way.
+  STACK_OF(SSL_CIPHER) *ciphers() { return ciphers_.get(); }
+
  private:
   // SSL_CIPHERs are maintained in a stack so they are easily accessible in the
   // form required for `SSL{_CTX}_get_ciphers`.
@@ -336,15 +362,24 @@
 const EVP_MD *ssl_get_handshake_digest(uint16_t version,
                                        const SSL_CIPHER *cipher);
 
-// ssl_create_cipher_list evaluates `rule_str`. It sets `*out_cipher_list` to a
-// newly-allocated `SSLCipherPreferenceList` containing the result. It returns
-// true on success and false on failure. If `strict` is true, nonsense will be
-// rejected. If false, nonsense will be silently ignored. An empty result is
-// considered an error regardless of `strict`. The resulting list will be
-// ordered based on having support for AES in hardware or not.
+// ssl_create_cipher_list evaluates `rule_str` to create the TLS 1.2 cipher
+// list. It sets `*out_cipher_list` to a newly-allocated
+// `SSLCipherPreferenceList` containing the result. It returns true on success
+// and false on failure. If `strict` is true, nonsense will be rejected. If
+// false, nonsense will be silently ignored. An empty result is considered an
+// error regardless of `strict`. The resulting list will be ordered based on
+// having support for AES in hardware or not.
 bool ssl_create_cipher_list(UniquePtr<SSLCipherPreferenceList> *out_cipher_list,
                             const char *rule_str, bool strict);
 
+// ssl_create_default_tls13_cipher_list populates the default TLS 1.3 cipher
+// list, clearing any previous contents in `*out_cipher_list` and replacing them
+// with the result. It returns true on success and false on failure. The
+// resulting list will be ordered based on having support for AES in hardware or
+// not.
+bool ssl_create_default_tls13_cipher_list(
+    SSLCipherPreferenceList *out_cipher_list);
+
 // ssl_cipher_auth_mask_for_key returns the mask of cipher `algorithm_auth`
 // values suitable for use with `key` in TLS 1.2 and below. `sign_ok` indicates
 // whether `key` may be used for signing.
@@ -366,19 +401,6 @@
 // it returns zero.
 size_t ssl_cipher_get_record_split_len(const SSL_CIPHER *cipher);
 
-// ssl_choose_tls13_cipher returns an `SSL_CIPHER` corresponding with the best
-// available from `cipher_suites` compatible with `version` and `policy`. It
-// returns NULL if there isn't a compatible cipher. `has_aes_hw` indicates if
-// the choice should be made as if support for AES in hardware is available.
-const SSL_CIPHER *ssl_choose_tls13_cipher(CBS cipher_suites, bool has_aes_hw,
-                                          uint16_t version,
-                                          enum ssl_compliance_policy_t policy);
-
-// ssl_tls13_cipher_meets_policy returns true if `cipher_id` is acceptable given
-// `policy`.
-bool ssl_tls13_cipher_meets_policy(uint16_t cipher_id,
-                                   enum ssl_compliance_policy_t policy);
-
 // ssl_cipher_is_deprecated returns true if `cipher` is deprecated.
 bool ssl_cipher_is_deprecated(const SSL_CIPHER *cipher);
 
@@ -3437,7 +3459,10 @@
   X509_VERIFY_PARAM *param = nullptr;
 
   // crypto
-  UniquePtr<SSLCipherPreferenceList> cipher_list;
+  UniquePtr<SSLCipherPreferenceList> cipher_list;  // for TLS 1.2 ciphers.
+
+  // Inherited from `SSL_CTX`.
+  SSLCipherPreferenceList tls13_cipher_list;
 
   // This is used to hold the local certificate used (i.e. the server
   // certificate for a server or the client certificate for a client).
@@ -3973,7 +3998,8 @@
   // quic_method is the method table corresponding to the QUIC hooks.
   const SSL_QUIC_METHOD *quic_method = nullptr;
 
-  UniquePtr<SSLCipherPreferenceList> cipher_list;
+  UniquePtr<SSLCipherPreferenceList> cipher_list;  // for TLS 1.2 ciphers.
+  SSLCipherPreferenceList tls13_cipher_list;
 
   X509_STORE *cert_store = nullptr;
   LHASH_OF(SSL_SESSION) *sessions = nullptr;
diff --git a/ssl/s3_both.cc b/ssl/s3_both.cc
index d885da9..10e0ad2 100644
--- a/ssl/s3_both.cc
+++ b/ssl/s3_both.cc
@@ -552,138 +552,4 @@
   }
 }
 
-namespace {
-
-class CipherScorer {
- public:
-  using Score = int;
-  static constexpr Score kMinScore = 0;
-
-  virtual ~CipherScorer() = default;
-
-  virtual Score Evaluate(const SSL_CIPHER *cipher) const = 0;
-};
-
-// AesHwCipherScorer scores cipher suites based on whether AES is supported in
-// hardware.
-class AesHwCipherScorer : public CipherScorer {
- public:
-  explicit AesHwCipherScorer(bool has_aes_hw) : aes_is_fine_(has_aes_hw) {}
-
-  virtual ~AesHwCipherScorer() override = default;
-
-  Score Evaluate(const SSL_CIPHER *a) const override {
-    return
-        // Something is always preferable to nothing.
-        1 +
-        // Either AES is fine, or else ChaCha20 is preferred.
-        ((aes_is_fine_ || a->algorithm_enc == SSL_CHACHA20POLY1305) ? 1 : 0);
-  }
-
- private:
-  const bool aes_is_fine_;
-};
-
-// CNsaCipherScorer prefers AES-256-GCM over AES-128-GCM over anything else.
-class CNsaCipherScorer : public CipherScorer {
- public:
-  virtual ~CNsaCipherScorer() override = default;
-
-  Score Evaluate(const SSL_CIPHER *a) const override {
-    if (a->protocol_id == SSL_CIPHER_AES_256_GCM_SHA384) {
-      return 3;
-    } else if (a->protocol_id == SSL_CIPHER_AES_128_GCM_SHA256) {
-      return 2;
-    }
-    return 1;
-  }
-};
-
-}  // namespace
-
-bool ssl_tls13_cipher_meets_policy(uint16_t cipher_id,
-                                   enum ssl_compliance_policy_t policy) {
-  switch (policy) {
-    case ssl_compliance_policy_none:
-    case ssl_compliance_policy_cnsa_202407:
-      return true;
-
-    case ssl_compliance_policy_fips_202205:
-      switch (cipher_id) {
-        case SSL_CIPHER_AES_128_GCM_SHA256:
-        case SSL_CIPHER_AES_256_GCM_SHA384:
-          return true;
-        case SSL_CIPHER_CHACHA20_POLY1305_SHA256:
-          return false;
-        default:
-          assert(false);
-          return false;
-      }
-
-    case ssl_compliance_policy_wpa3_192_202304:
-    case ssl_compliance_policy_cnsa1_202603:
-    case ssl_compliance_policy_cnsa2_202603:
-      switch (cipher_id) {
-        case SSL_CIPHER_AES_256_GCM_SHA384:
-          return true;
-        case SSL_CIPHER_AES_128_GCM_SHA256:
-        case SSL_CIPHER_CHACHA20_POLY1305_SHA256:
-          return false;
-        default:
-          assert(false);
-          return false;
-      }
-  }
-
-  assert(false);
-  return false;
-}
-
-const SSL_CIPHER *ssl_choose_tls13_cipher(CBS cipher_suites, bool has_aes_hw,
-                                          uint16_t version,
-                                          enum ssl_compliance_policy_t policy) {
-  if (CBS_len(&cipher_suites) % 2 != 0) {
-    return nullptr;
-  }
-
-  const SSL_CIPHER *best = nullptr;
-  AesHwCipherScorer aes_hw_scorer(has_aes_hw);
-  CNsaCipherScorer cnsa_scorer;
-  CipherScorer *const scorer =
-      (policy == ssl_compliance_policy_cnsa_202407)
-          ? static_cast<CipherScorer *>(&cnsa_scorer)
-          : static_cast<CipherScorer *>(&aes_hw_scorer);
-  CipherScorer::Score best_score = CipherScorer::kMinScore;
-
-  while (CBS_len(&cipher_suites) > 0) {
-    uint16_t cipher_suite;
-    if (!CBS_get_u16(&cipher_suites, &cipher_suite)) {
-      return nullptr;
-    }
-
-    // Limit to TLS 1.3 ciphers we know about.
-    const SSL_CIPHER *candidate = SSL_get_cipher_by_value(cipher_suite);
-    if (candidate == nullptr ||
-        SSL_CIPHER_get_min_version(candidate) > version ||
-        SSL_CIPHER_get_max_version(candidate) < version) {
-      continue;
-    }
-
-    if (!ssl_tls13_cipher_meets_policy(SSL_CIPHER_get_protocol_id(candidate),
-                                       policy)) {
-      continue;
-    }
-
-    const CipherScorer::Score candidate_score = scorer->Evaluate(candidate);
-    // `candidate_score` must be larger to displace the current choice. That way
-    // the client's order controls between ciphers with an equal score.
-    if (candidate_score > best_score) {
-      best = candidate;
-      best_score = candidate_score;
-    }
-  }
-
-  return best;
-}
-
 BSSL_NAMESPACE_END
diff --git a/ssl/ssl_cipher.cc b/ssl/ssl_cipher.cc
index 4b7a78b..c36c194 100644
--- a/ssl/ssl_cipher.cc
+++ b/ssl/ssl_cipher.cc
@@ -20,6 +20,7 @@
 #include <string.h>
 
 #include <iterator>
+#include <optional>
 
 #include <openssl/aead.h>
 #include <openssl/err.h>
@@ -605,18 +606,81 @@
   *head = curr;
 }
 
+// Helper to iterate over a client cipher list and find a given cipher protocol
+// ID. Returns the index of the cipher, if found in `cipher_list`, or returns
+// std::nullopt if not found.
+static std::optional<size_t> FindProtocolID(CBS *cipher_list,
+                                            uint16_t cipher_id) {
+  assert(CBS_len(cipher_list) % 2 == 0);
+  if (CBS_len(cipher_list) == 0) {
+    return std::nullopt;
+  }
+  size_t cur_index = 0;
+  while (CBS_len(cipher_list) > 0) {
+    uint16_t cipher_suite;
+    if (!CBS_get_u16(cipher_list, &cipher_suite)) {
+      return std::nullopt;
+    }
+    if (cipher_suite == cipher_id) {
+      return cur_index;
+    }
+    ++cur_index;
+  }
+  return std::nullopt;
+}
+
 bool SSLCipherPreferenceList::Init(UniquePtr<STACK_OF(SSL_CIPHER)> ciphers,
                                    Array<bool> in_group_flags) {
   if (sk_SSL_CIPHER_num(ciphers.get()) != in_group_flags.size()) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return false;
   }
+  // The last element has no next element to be in a group with.
+  if (!in_group_flags.empty() && in_group_flags.back()) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
 
   ciphers_ = std::move(ciphers);
   in_group_flags_ = std::move(in_group_flags);
   return true;
 }
 
+bool SSLCipherPreferenceList::Init(Span<const uint16_t> cipher_ids,
+                                   Span<const bool> in_group_flags) {
+  if (cipher_ids.size() != in_group_flags.size()) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+
+  UniquePtr<STACK_OF(SSL_CIPHER)> ciphers(sk_SSL_CIPHER_new_null());
+  if (!ciphers) {
+    return false;
+  }
+  for (uint16_t cipher_id : cipher_ids) {
+    const SSL_CIPHER *cipher = SSL_get_cipher_by_value(cipher_id);
+    if (cipher == nullptr) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_UNKNOWN_CIPHER_TYPE);
+      return false;
+    }
+    if (!sk_SSL_CIPHER_push(ciphers.get(), cipher)) {
+      return false;
+    }
+  }
+
+  Array<bool> flags;
+  if (!flags.CopyFrom(in_group_flags)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return false;
+  }
+  return Init(std::move(ciphers), std::move(flags));
+}
+
+void SSLCipherPreferenceList::Reset() {
+  sk_SSL_CIPHER_zero(ciphers_.get());
+  in_group_flags_.Reset();
+}
+
 bool SSLCipherPreferenceList::CopyFrom(const SSLCipherPreferenceList &other) {
   UniquePtr<STACK_OF(SSL_CIPHER)> other_ciphers(
       sk_SSL_CIPHER_dup(other.ciphers()));
@@ -645,6 +709,76 @@
   in_group_flags_.Shrink(size());
 }
 
+bool SSLCipherPreferenceList::Contains(uint16_t cipher_id) const {
+  for (const SSL_CIPHER *cipher : ciphers_.get()) {
+    if (cipher->protocol_id == cipher_id) {
+      return true;
+    }
+  }
+  return false;
+}
+
+const SSL_CIPHER *SSLCipherPreferenceList::ChooseCipher(
+    const CBS *client_cipher_list, bool prioritize_client_pref,
+    uint16_t version, uint32_t mask_k, uint32_t mask_a) const {
+  if (CBS_len(client_cipher_list) % 2 != 0) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_ERROR_IN_RECEIVED_CIPHER_LIST);
+    return nullptr;
+  }
+
+  // Index of the best matching cipher suite found so far, indexed into
+  // `client_cipher_list`.
+  std::optional<size_t> best_index = std::nullopt;
+  const SSL_CIPHER *best_cipher = nullptr;
+
+  // Iterate over our list (the server preference list) and check for each
+  // cipher in the client's list.
+  for (size_t i = 0; i < size(); ++i) {
+    const SSL_CIPHER *const c = sk_SSL_CIPHER_value(ciphers_.get(), i);
+    bool in_group = in_group_flags_[i];
+    // If prioritizing the client preference list, treat all of the server's
+    // allowed ciphers as a single equipreference group so that the client's
+    // preferences dictate the choice.
+    if (prioritize_client_pref) {
+      in_group = (i < size() - 1);
+    }
+
+    if (version >= SSL_CIPHER_get_min_version(c) &&
+        version <= SSL_CIPHER_get_max_version(c) &&
+        (c->algorithm_mkey & mask_k) != 0 &&
+        (c->algorithm_auth & mask_a) != 0) {
+      CBS copy = *client_cipher_list;
+      std::optional<size_t> client_list_index =
+          FindProtocolID(&copy, c->protocol_id);
+      // Within a group, the client's preference order applies.
+      if (client_list_index.has_value() &&
+          (!best_index.has_value() || *best_index > *client_list_index)) {
+        best_index = *client_list_index;
+        best_cipher = c;
+      }
+    }
+
+    // Always evaluate a whole equipreference group.
+    if (in_group) {
+      continue;
+    }
+    // We are about to leave a (possibly singleton) group. If we have a match,
+    // return it because we will only see less-preferred ciphers if we keep
+    // going.
+    if (best_index.has_value()) {
+      assert(best_cipher != nullptr);
+      return best_cipher;
+    }
+  }
+
+  // The final cipher suite must end a group, so, if we found a match, we must
+  // have returned early above.
+  assert(!best_index.has_value());
+  assert(best_cipher == nullptr);
+  OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_CIPHER);
+  return nullptr;
+}
+
 bool ssl_cipher_is_deprecated(const SSL_CIPHER *cipher) {
   return cipher->protocol_id ==
              SSL_CIPHER_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 ||
@@ -1144,6 +1278,47 @@
   return true;
 }
 
+bool ssl_create_default_tls13_cipher_list(
+    SSLCipherPreferenceList *out_cipher_list) {
+  // If we have AES hardware:
+  // For a client: AES-128 > AES-256 > ChaCha20.
+  // For a server: (AES-128 | AES-256 | ChaCha20), i.e. defer to client
+  // preference.
+  static const uint16_t kCiphersAESHardware[] = {
+      SSL_CIPHER_AES_128_GCM_SHA256,
+      SSL_CIPHER_AES_256_GCM_SHA384,
+      SSL_CIPHER_CHACHA20_POLY1305_SHA256,
+  };
+  static const bool kInGroupFlagsAESHardware[] = {
+      true,
+      true,
+      false,
+  };
+  // If we do not have AES hardware:
+  // For a client: ChaCha20 > AES-128 > AES-256.
+  // For a server: ChaCha20 > (AES-128 | AES-256).
+  static const uint16_t kCiphersNoAESHardware[] = {
+      SSL_CIPHER_CHACHA20_POLY1305_SHA256,
+      SSL_CIPHER_AES_128_GCM_SHA256,
+      SSL_CIPHER_AES_256_GCM_SHA384,
+  };
+  static const bool kInGroupFlagsNoAESHardware[] = {
+      false,
+      true,
+      false,
+  };
+
+  Span<const uint16_t> ciphers = EVP_has_aes_hardware()
+                                     ? Span(kCiphersAESHardware)
+                                     : Span(kCiphersNoAESHardware);
+  Span<const bool> in_group_flags = EVP_has_aes_hardware()
+                                        ? Span(kInGroupFlagsAESHardware)
+                                        : Span(kInGroupFlagsNoAESHardware);
+
+  out_cipher_list->Reset();
+  return out_cipher_list->Init(ciphers, in_group_flags);
+}
+
 uint32_t ssl_cipher_auth_mask_for_key(const EVP_PKEY *key, bool sign_ok) {
   switch (EVP_PKEY_id(key)) {
     case EVP_PKEY_RSA:
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index ff885c9..b7d9980 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -426,6 +426,7 @@
   }
 
   if (!SSL_CTX_set_strict_cipher_list(ret.get(), SSL_DEFAULT_CIPHER_LIST) ||
+      !ssl_create_default_tls13_cipher_list(&ret->tls13_cipher_list) ||
       // Lock the SSL_CTX to the specified version, for compatibility with
       // legacy uses of SSL_METHOD.
       !SSL_CTX_set_max_proto_version(ret.get(), method->version) ||
@@ -514,6 +515,10 @@
   ssl->config->permute_extensions = ctx_impl->permute_extensions;
   ssl->config->compliance_policy = ctx_impl->compliance_policy;
 
+  if (!ssl->config->tls13_cipher_list.CopyFrom(ctx_impl->tls13_cipher_list)) {
+    return nullptr;
+  }
+
   if (!ssl->config->supported_group_list.CopyFrom(
           ctx_impl->supported_group_list) ||
       !ssl->config->supported_group_list_flags.CopyFrom(
@@ -3532,6 +3537,15 @@
     "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:"
     "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384";
 
+static const uint16_t kTLS13Ciphers[] = {
+    SSL_CIPHER_AES_128_GCM_SHA256,
+    SSL_CIPHER_AES_256_GCM_SHA384,
+};
+static const bool kTLS13CiphersInGroup[] = {
+    true,
+    false,
+};
+
 static int Configure(SSLContext *ctx) {
   ctx->compliance_policy = ssl_compliance_policy_fips_202205;
 
@@ -3548,6 +3562,8 @@
       // Encrypt-then-MAC extension is required for all CBC cipher suites and so
       // it's easier to drop them.
       SSL_CTX_set_strict_cipher_list(ctx, kTLS12Ciphers) &&
+      ctx->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                  Span(kTLS13CiphersInGroup)) &&
       SSL_CTX_set1_group_ids(ctx, kGroups, std::size(kGroups)) &&
       SSL_CTX_set_signing_algorithm_prefs(ctx, kSigAlgs, std::size(kSigAlgs)) &&
       SSL_CTX_set_verify_algorithm_prefs(ctx, kSigAlgs, std::size(kSigAlgs));
@@ -3560,6 +3576,8 @@
   return SSL_set_min_proto_version(ssl, TLS1_2_VERSION) &&
          SSL_set_max_proto_version(ssl, TLS1_3_VERSION) &&
          SSL_set_strict_cipher_list(ssl, kTLS12Ciphers) &&
+         ssl->config->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                             Span(kTLS13CiphersInGroup)) &&
          SSL_set1_group_ids(ssl, kGroups, std::size(kGroups)) &&
          SSL_set_signing_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs)) &&
          SSL_set_verify_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs));
@@ -3585,6 +3603,13 @@
     "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:"
     "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384";
 
+static const uint16_t kTLS13Ciphers[] = {
+    SSL_CIPHER_AES_256_GCM_SHA384,
+};
+static const bool kTLS13CiphersInGroup[] = {
+    false,
+};
+
 static int Configure(SSLContext *ctx) {
   ctx->compliance_policy = ssl_compliance_policy_wpa3_192_202304;
 
@@ -3592,6 +3617,8 @@
          SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) &&
          SSL_CTX_set_strict_cipher_list(ctx, kTLS12Ciphers) &&
          SSL_CTX_set1_group_ids(ctx, kGroups, std::size(kGroups)) &&
+         ctx->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                     Span(kTLS13CiphersInGroup)) &&
          SSL_CTX_set_signing_algorithm_prefs(ctx, kSigAlgs,
                                              std::size(kSigAlgs)) &&
          SSL_CTX_set_verify_algorithm_prefs(ctx, kSigAlgs, std::size(kSigAlgs));
@@ -3603,6 +3630,8 @@
   return SSL_set_min_proto_version(ssl, TLS1_2_VERSION) &&
          SSL_set_max_proto_version(ssl, TLS1_3_VERSION) &&
          SSL_set_strict_cipher_list(ssl, kTLS12Ciphers) &&
+         ssl->config->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                             Span(kTLS13CiphersInGroup)) &&
          SSL_set1_group_ids(ssl, kGroups, std::size(kGroups)) &&
          SSL_set_signing_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs)) &&
          SSL_set_verify_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs));
@@ -3612,14 +3641,27 @@
 
 namespace cnsa202407 {
 
+static const uint16_t kTLS13Ciphers[] = {
+    SSL_CIPHER_AES_256_GCM_SHA384,
+    SSL_CIPHER_AES_128_GCM_SHA256,
+    SSL_CIPHER_CHACHA20_POLY1305_SHA256,
+};
+static const bool kTLS13CiphersInGroup[] = {
+    false,
+    false,
+    false,
+};
+
 static int Configure(SSLContext *ctx) {
   ctx->compliance_policy = ssl_compliance_policy_cnsa_202407;
-  return 1;
+  return ctx->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                     Span(kTLS13CiphersInGroup));
 }
 
 static int Configure(SSLImpl *ssl) {
   ssl->config->compliance_policy = ssl_compliance_policy_cnsa_202407;
-  return 1;
+  return ssl->config->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                             Span(kTLS13CiphersInGroup));
 }
 
 }  // namespace cnsa202407
@@ -3644,12 +3686,21 @@
     "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:"
     "TLS_RSA_WITH_AES_256_GCM_SHA384";
 
+static const uint16_t kTLS13Ciphers[] = {
+    SSL_CIPHER_AES_256_GCM_SHA384,
+};
+static const bool kTLS13CiphersInGroup[] = {
+    false,
+};
+
 static int Configure(SSLContext *ctx) {
   ctx->compliance_policy = ssl_compliance_policy_cnsa1_202603;
 
   return SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION) &&
          SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) &&
          SSL_CTX_set_strict_cipher_list(ctx, kTLS12Ciphers) &&
+         ctx->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                     Span(kTLS13CiphersInGroup)) &&
          SSL_CTX_set1_group_ids(ctx, kGroups, std::size(kGroups)) &&
          SSL_CTX_set_options(ctx, kOptions) &&
          SSL_CTX_set_signing_algorithm_prefs(ctx, kSigAlgs,
@@ -3663,6 +3714,8 @@
   return SSL_set_min_proto_version(ssl, TLS1_2_VERSION) &&
          SSL_set_max_proto_version(ssl, TLS1_3_VERSION) &&
          SSL_set_strict_cipher_list(ssl, kTLS12Ciphers) &&
+         ssl->config->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                             Span(kTLS13CiphersInGroup)) &&
          SSL_set1_group_ids(ssl, kGroups, std::size(kGroups)) &&
          SSL_set_options(ssl, kOptions) &&
          SSL_set_signing_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs)) &&
@@ -3683,11 +3736,20 @@
     SSL_SIGN_RSA_PKCS1_SHA384,
 };
 
+static const uint16_t kTLS13Ciphers[] = {
+    SSL_CIPHER_AES_256_GCM_SHA384,
+};
+static const bool kTLS13CiphersInGroup[] = {
+    false,
+};
+
 static int Configure(SSLContext *ctx) {
   ctx->compliance_policy = ssl_compliance_policy_cnsa2_202603;
 
   return SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION) &&
          SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION) &&
+         ctx->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                     Span(kTLS13CiphersInGroup)) &&
          SSL_CTX_set1_group_ids(ctx, kGroups, std::size(kGroups)) &&
          SSL_CTX_set_signing_algorithm_prefs(ctx, kSigAlgs,
                                              std::size(kSigAlgs)) &&
@@ -3699,6 +3761,8 @@
 
   return SSL_set_min_proto_version(ssl, TLS1_3_VERSION) &&
          SSL_set_max_proto_version(ssl, TLS1_3_VERSION) &&
+         ssl->config->tls13_cipher_list.Init(Span(kTLS13Ciphers),
+                                             Span(kTLS13CiphersInGroup)) &&
          SSL_set1_group_ids(ssl, kGroups, std::size(kGroups)) &&
          SSL_set_signing_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs)) &&
          SSL_set_verify_algorithm_prefs(ssl, kSigAlgs, std::size(kSigAlgs));
diff --git a/ssl/test/runner/cipher_suite_tests.go b/ssl/test/runner/cipher_suite_tests.go
index 4880d37..9f35445 100644
--- a/ssl/test/runner/cipher_suite_tests.go
+++ b/ssl/test/runner/cipher_suite_tests.go
@@ -283,6 +283,22 @@
 	})
 
 	testCases = append(testCases, testCase{
+		name: "UnsupportedCipherSuite-TLS13",
+		config: Config{
+			MaxVersion:   VersionTLS13,
+			CipherSuites: []uint16{TLS_AES_128_GCM_SHA256},
+			Bugs: ProtocolBugs{
+				IgnorePeerCipherPreferences: true,
+			},
+		},
+		// This compliance policy causes the client to advertise only
+		// TLS_AES_256_GCM_SHA384.
+		flags:         []string{"-wpa-202304"},
+		shouldFail:    true,
+		expectedError: ":WRONG_CIPHER_RETURNED:",
+	})
+
+	testCases = append(testCases, testCase{
 		name: "ServerHelloBogusCipher",
 		config: Config{
 			MaxVersion: VersionTLS12,
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index f134ebb..f0caf83 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -204,15 +204,12 @@
     return ssl_hs_error;
   }
 
-  // The cipher suite must be one we offered. We currently offer all supported
-  // TLS 1.3 ciphers unless policy controls limited it. So we check the version
-  // and that it's ok per policy.
+  // The cipher suite must be one we offered, and supported for the version.
   const SSL_CIPHER *cipher = SSL_get_cipher_by_value(server_hello.cipher_suite);
   if (cipher == nullptr ||
       SSL_CIPHER_get_min_version(cipher) > ssl_protocol_version(ssl) ||
       SSL_CIPHER_get_max_version(cipher) < ssl_protocol_version(ssl) ||
-      !ssl_tls13_cipher_meets_policy(SSL_CIPHER_get_protocol_id(cipher),
-                                     ssl->config->compliance_policy)) {
+      !ssl->config->tls13_cipher_list.Contains(server_hello.cipher_suite)) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_WRONG_CIPHER_RETURNED);
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
     return ssl_hs_error;
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 6d2031f..8119c3b 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -136,18 +136,6 @@
   return 1;
 }
 
-static const SSL_CIPHER *choose_tls13_cipher(
-    const SSLImpl *ssl, const SSL_CLIENT_HELLO *client_hello) {
-  CBS cipher_suites;
-  CBS_init(&cipher_suites, client_hello->cipher_suites,
-           client_hello->cipher_suites_len);
-
-  const uint16_t version = ssl_protocol_version(ssl);
-
-  return ssl_choose_tls13_cipher(cipher_suites, EVP_has_aes_hardware(), version,
-                                 ssl->config->compliance_policy);
-}
-
 static bool add_new_session_tickets(SSL_HANDSHAKE *hs, bool *out_sent_tickets) {
   SSLImpl *const ssl = hs->ssl;
   if (  // If the client doesn't accept resumption with PSK_DHE_KE, don't send a
@@ -388,7 +376,12 @@
   }
 
   // Negotiate the cipher suite. This must happen before negotiating PSKs.
-  hs->new_cipher = choose_tls13_cipher(ssl, &client_hello);
+  CBS client_cipher_list;
+  CBS_init(&client_cipher_list, client_hello.cipher_suites,
+           client_hello.cipher_suites_len);
+  hs->new_cipher = ssl->config->tls13_cipher_list.ChooseCipher(
+      &client_cipher_list, /*prioritize_client_pref=*/false,
+      ssl_protocol_version(ssl), SSL_kGENERIC, SSL_aGENERIC);
   if (hs->new_cipher == nullptr) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_NO_SHARED_CIPHER);
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_HANDSHAKE_FAILURE);