Expose reject_unusable_ech_config via QuicSSLConfig

This is necessary for Chromium's EchMode::kStrict to correctly configure BoringSSL when used by QUIC, via QuicChromiumClientSession. See https://boringssl-review.googlesource.com/c/boringssl/+/99327 and https://crrev.com/c/8229533. This is how ECH GREASE is already plumbed.

Protected by FLAGS_quic_reloadable_flag_quic_reject_unusable_ech_config.

PiperOrigin-RevId: 962850299
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index e4e6774..412762e 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -57,6 +57,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_priority_respect_incremental, false, false, "If true, respect the incremental parameter of each stream in QuicWriteBlockedList.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_receive_ack_frequency, false, false, "When true, advertises support for ACK_FREQUENCY and IMMEDIATE_ACK from draft-ietf-quic-ack-frequency-10 and processes them correctly.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_reject_empty_cid_in_ncid, false, true, "If true, QUIC framer will reject NEW_CONNECTION_ID frames with empty connection IDs.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_reject_unusable_ech_config, false, true, "When true, calls SSL_set_reject_unusable_ech_config in TlsClientHandshaker.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_require_handshake_confirmation, true, true, "If true, require handshake confirmation for QUIC connections, functionally disabling 0-rtt handshakes.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_test_peer_addr_change_after_normalize, false, false, "If true, QuicConnection::ProcessValidatedPacket will use normalized address to test peer address changes.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_false, false, false, "A testonly reloadable flag that will always default to false.")
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index 28a28b3..2b82db8 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -492,7 +492,8 @@
          lhs.signing_algorithm_prefs == rhs.signing_algorithm_prefs &&
          lhs.client_cert_mode == rhs.client_cert_mode &&
          lhs.ech_config_list == rhs.ech_config_list &&
-         lhs.ech_grease_enabled == rhs.ech_grease_enabled;
+         lhs.ech_grease_enabled == rhs.ech_grease_enabled &&
+         lhs.reject_unusable_ech_config == rhs.reject_unusable_ech_config;
 }
 
 #undef RETURN_STRING_LITERAL  // undef for jumbo builds
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index 1ad2b56..d944929 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -902,6 +902,10 @@
   // As a client, whether ECH GREASE is enabled. If `ech_config_list` is
   // not empty, this value does nothing.
   bool ech_grease_enabled = false;
+  // As a client, whether set BoringSSL's reject_unusable_ech_config. See
+  // BoringSSL's documentation for SSL_set_reject_unusable_ech_config in
+  // https://boringssl.googlesource.com/boringssl/+/HEAD/include/openssl/ssl.h.
+  bool reject_unusable_ech_config = false;
   // If not nullopt, the TLS Trust Anchor IDs to send in the TLS handshake. (See
   // https://tlswg.org/tls-trust-anchor-ids/draft-ietf-tls-trust-anchor-ids.html.)
   // The value should be a series of Trust Anchor IDs in wire format (a series
diff --git a/quiche/quic/core/quic_types_test.cc b/quiche/quic/core/quic_types_test.cc
index 075054a..ddea65d 100644
--- a/quiche/quic/core/quic_types_test.cc
+++ b/quiche/quic/core/quic_types_test.cc
@@ -20,5 +20,16 @@
             "Unknown (255)");
 }
 
+TEST(QuicSSLConfigTest, Equality) {
+  QuicSSLConfig config1;
+  QuicSSLConfig config2;
+  EXPECT_EQ(config1, config2);
+
+  config1.reject_unusable_ech_config = true;
+  EXPECT_NE(config1, config2);
+  config2.reject_unusable_ech_config = true;
+  EXPECT_EQ(config1, config2);
+}
+
 }  // namespace
 }  // namespace quic
diff --git a/quiche/quic/core/tls_client_handshaker.cc b/quiche/quic/core/tls_client_handshaker.cc
index 9e90268..e4c53f6 100644
--- a/quiche/quic/core/tls_client_handshaker.cc
+++ b/quiche/quic/core/tls_client_handshaker.cc
@@ -162,6 +162,11 @@
 
   SSL_set_enable_ech_grease(ssl(),
                             tls_connection_.ssl_config().ech_grease_enabled);
+  if (GetQuicReloadableFlag(quic_reject_unusable_ech_config)) {
+    QUIC_RELOADABLE_FLAG_COUNT(quic_reject_unusable_ech_config);
+    SSL_set_reject_unusable_ech_config(
+        ssl(), tls_connection_.ssl_config().reject_unusable_ech_config);
+  }
   if (!tls_connection_.ssl_config().ech_config_list.empty() &&
       !SSL_set1_ech_config_list(
           ssl(),
diff --git a/quiche/quic/core/tls_client_handshaker_test.cc b/quiche/quic/core/tls_client_handshaker_test.cc
index 7c63be1..a9b11f2 100644
--- a/quiche/quic/core/tls_client_handshaker_test.cc
+++ b/quiche/quic/core/tls_client_handshaker_test.cc
@@ -942,6 +942,56 @@
   stream()->CryptoConnect();
 }
 
+TEST_P(TlsClientHandshakerTest, ECHRejectUnusableConfigFlagEnabled) {
+  // When reject_unusable_ech_config is enabled, and no usable ECHConfig is
+  // available, the client should fail before sending a ClientHello.
+  SetQuicReloadableFlag(quic_reject_unusable_ech_config, true);
+  ssl_config_.emplace();
+  ssl_config_->reject_unusable_ech_config = true;
+  CreateConnection();
+  EXPECT_CALL(*connection_, CloseConnection(QUIC_HANDSHAKE_FAILED, _, _));
+  stream()->CryptoConnect();
+}
+
+TEST_P(TlsClientHandshakerTest, ECHRejectUnusableConfigFlagDisabled) {
+  // When the flag is disabled, reject_unusable_ech_config is ignored and the
+  // handshake succeeds without ECH.
+  SetQuicReloadableFlag(quic_reject_unusable_ech_config, false);
+  ssl_config_.emplace();
+  ssl_config_->reject_unusable_ech_config = true;
+  CreateConnection();
+
+  CompleteCryptoHandshake();
+  EXPECT_TRUE(stream()->version().IsIetfQuic());
+  EXPECT_TRUE(stream()->encryption_established());
+  EXPECT_TRUE(stream()->one_rtt_keys_available());
+  EXPECT_FALSE(stream()->crypto_negotiated_params().encrypted_client_hello);
+}
+
+TEST_P(TlsClientHandshakerTest, ECHWithRejectUnusableConfig) {
+  SetQuicReloadableFlag(quic_reject_unusable_ech_config, true);
+  ssl_config_.emplace();
+  bssl::UniquePtr<SSL_ECH_KEYS> ech_keys =
+      MakeTestEchKeys("public-name.example", /*max_name_len=*/64,
+                      &ssl_config_->ech_config_list);
+  ASSERT_TRUE(ech_keys);
+  ssl_config_->reject_unusable_ech_config = true;
+
+  // Configure the server to use the test ECH keys.
+  ASSERT_TRUE(
+      SSL_CTX_set1_ech_keys(server_crypto_config_->ssl_ctx(), ech_keys.get()));
+
+  // Recreate the client to pick up the new `ssl_config_`.
+  CreateConnection();
+
+  // The handshake should complete and negotiate ECH.
+  CompleteCryptoHandshake();
+  EXPECT_TRUE(stream()->version().IsIetfQuic());
+  EXPECT_TRUE(stream()->encryption_established());
+  EXPECT_TRUE(stream()->one_rtt_keys_available());
+  EXPECT_TRUE(stream()->crypto_negotiated_params().encrypted_client_hello);
+}
+
 TEST_P(TlsClientHandshakerTest, ECHWrongKeys) {
   ssl_config_.emplace();
   bssl::UniquePtr<SSL_ECH_KEYS> ech_keys1 =