Parse and log cert compression algos from CHLO.

Protected by FLAGS_quic_reloadable_flag_quic_parse_cert_compression_algos_from_chlo.

PiperOrigin-RevId: 651509489
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 3871993..fe07be7 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -41,6 +41,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_no_write_control_frame_upon_connection_close, false, true, "If trrue, early return before write control frame in OnCanWrite() if the connection is already closed.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_no_write_control_frame_upon_connection_close2, false, false, "If true, QuicSession will block outgoing control frames when the connection is closed.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_pacing_remove_non_initial_burst, false, false, "If true, remove the non-initial burst in QUIC PacingSender.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_parse_cert_compression_algos_from_chlo, false, false, "If true, parse offered cert compression algorithms from received CHLOs.")
 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_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_send_placeholder_ticket_when_encrypt_ticket_fails, false, true, "If true, when TicketCrypter fails to encrypt a session ticket, quic::TlsServerHandshaker will send a placeholder ticket, instead of an empty one, to the client.")
diff --git a/quiche/quic/core/quic_buffered_packet_store.cc b/quiche/quic/core/quic_buffered_packet_store.cc
index cb990ab..e2fb225 100644
--- a/quiche/quic/core/quic_buffered_packet_store.cc
+++ b/quiche/quic/core/quic_buffered_packet_store.cc
@@ -317,6 +317,7 @@
     const QuicConnectionId& connection_id, const ParsedQuicVersion& version,
     const QuicReceivedPacket& packet,
     std::vector<uint16_t>* out_supported_groups,
+    std::vector<uint16_t>* out_cert_compression_algos,
     std::vector<std::string>* out_alpns, std::string* out_sni,
     bool* out_resumption_attempted, bool* out_early_data_attempted,
     std::optional<uint8_t>* tls_alert) {
@@ -337,6 +338,7 @@
   }
   const TlsChloExtractor& tls_chlo_extractor = it->second.tls_chlo_extractor;
   *out_supported_groups = tls_chlo_extractor.supported_groups();
+  *out_cert_compression_algos = tls_chlo_extractor.cert_compression_algos();
   *out_alpns = tls_chlo_extractor.alpns();
   *out_sni = tls_chlo_extractor.server_name();
   *out_resumption_attempted = tls_chlo_extractor.resumption_attempted();
diff --git a/quiche/quic/core/quic_buffered_packet_store.h b/quiche/quic/core/quic_buffered_packet_store.h
index 354c9cf..db9b74f 100644
--- a/quiche/quic/core/quic_buffered_packet_store.h
+++ b/quiche/quic/core/quic_buffered_packet_store.h
@@ -146,6 +146,7 @@
       const QuicConnectionId& connection_id, const ParsedQuicVersion& version,
       const QuicReceivedPacket& packet,
       std::vector<uint16_t>* out_supported_groups,
+      std::vector<uint16_t>* out_cert_compression_algos,
       std::vector<std::string>* out_alpns, std::string* out_sni,
       bool* out_resumption_attempted, bool* out_early_data_attempted,
       std::optional<uint8_t>* tls_alert);
diff --git a/quiche/quic/core/quic_buffered_packet_store_test.cc b/quiche/quic/core/quic_buffered_packet_store_test.cc
index 7f38170..85ed960 100644
--- a/quiche/quic/core/quic_buffered_packet_store_test.cc
+++ b/quiche/quic/core/quic_buffered_packet_store_test.cc
@@ -576,6 +576,7 @@
   QuicConnectionId connection_id = TestConnectionId(1);
   std::vector<std::string> alpns;
   std::vector<uint16_t> supported_groups;
+  std::vector<uint16_t> cert_compression_algos;
   std::string sni;
   bool resumption_attempted = false;
   bool early_data_attempted = false;
@@ -590,8 +591,9 @@
 
   // The packet in 'packet_' is not a TLS CHLO packet.
   EXPECT_FALSE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, packet_, &supported_groups, &alpns, &sni,
-      &resumption_attempted, &early_data_attempted, &tls_alert));
+      connection_id, valid_version_, packet_, &supported_groups,
+      &cert_compression_algos, &alpns, &sni, &resumption_attempted,
+      &early_data_attempted, &tls_alert));
 
   store_.DiscardPackets(connection_id);
 
@@ -613,11 +615,13 @@
 
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   EXPECT_FALSE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, *packets[0], &supported_groups, &alpns,
-      &sni, &resumption_attempted, &early_data_attempted, &tls_alert));
+      connection_id, valid_version_, *packets[0], &supported_groups,
+      &cert_compression_algos, &alpns, &sni, &resumption_attempted,
+      &early_data_attempted, &tls_alert));
   EXPECT_TRUE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, *packets[1], &supported_groups, &alpns,
-      &sni, &resumption_attempted, &early_data_attempted, &tls_alert));
+      connection_id, valid_version_, *packets[1], &supported_groups,
+      &cert_compression_algos, &alpns, &sni, &resumption_attempted,
+      &early_data_attempted, &tls_alert));
 
   EXPECT_THAT(alpns, ElementsAre(AlpnForVersion(valid_version_)));
   EXPECT_FALSE(supported_groups.empty());
diff --git a/quiche/quic/core/quic_dispatcher.cc b/quiche/quic/core/quic_dispatcher.cc
index 4ced4aa..dfa4171 100644
--- a/quiche/quic/core/quic_dispatcher.cc
+++ b/quiche/quic/core/quic_dispatcher.cc
@@ -601,6 +601,7 @@
     bool has_full_tls_chlo = false;
     std::string sni;
     std::vector<uint16_t> supported_groups;
+    std::vector<uint16_t> cert_compression_algos;
     std::vector<std::string> alpns;
     bool resumption_attempted = false, early_data_attempted = false;
     if (buffered_packets_.HasBufferedPackets(
@@ -609,8 +610,9 @@
       // use the associated TlsChloExtractor to parse this packet.
       has_full_tls_chlo = buffered_packets_.IngestPacketForTlsChloExtraction(
           packet_info.destination_connection_id, packet_info.version,
-          packet_info.packet, &supported_groups, &alpns, &sni,
-          &resumption_attempted, &early_data_attempted, &result.tls_alert);
+          packet_info.packet, &supported_groups, &cert_compression_algos,
+          &alpns, &sni, &resumption_attempted, &early_data_attempted,
+          &result.tls_alert);
     } else {
       // If we do not have a BufferedPacketList for this connection ID,
       // create a single-use one to check whether this packet contains a
@@ -621,6 +623,7 @@
         // This packet contains a full single-packet CHLO.
         has_full_tls_chlo = true;
         supported_groups = tls_chlo_extractor.supported_groups();
+        cert_compression_algos = tls_chlo_extractor.cert_compression_algos();
         alpns = tls_chlo_extractor.alpns();
         sni = tls_chlo_extractor.server_name();
         resumption_attempted = tls_chlo_extractor.resumption_attempted();
@@ -648,6 +651,7 @@
     ParsedClientHello& parsed_chlo = result.parsed_chlo.emplace();
     parsed_chlo.sni = std::move(sni);
     parsed_chlo.supported_groups = std::move(supported_groups);
+    parsed_chlo.cert_compression_algos = std::move(cert_compression_algos);
     parsed_chlo.alpns = std::move(alpns);
     if (packet_info.retry_token.has_value()) {
       parsed_chlo.retry_token = std::string(*packet_info.retry_token);
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index e1507ad..43b5ace 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -422,8 +422,9 @@
 
 bool operator==(const ParsedClientHello& a, const ParsedClientHello& b) {
   return a.sni == b.sni && a.uaid == b.uaid &&
-         a.supported_groups == b.supported_groups && a.alpns == b.alpns &&
-         a.retry_token == b.retry_token &&
+         a.supported_groups == b.supported_groups &&
+         a.cert_compression_algos == b.cert_compression_algos &&
+         a.alpns == b.alpns && a.retry_token == b.retry_token &&
          a.resumption_attempted == b.resumption_attempted &&
          a.early_data_attempted == b.early_data_attempted;
 }
@@ -434,6 +435,8 @@
      << ", alpns:" << quiche::PrintElements(parsed_chlo.alpns)
      << ", supported_groups:"
      << quiche::PrintElements(parsed_chlo.supported_groups)
+     << ", cert_compression_algos:"
+     << quiche::PrintElements(parsed_chlo.cert_compression_algos)
      << ", resumption_attempted:" << parsed_chlo.resumption_attempted
      << ", early_data_attempted:" << parsed_chlo.early_data_attempted
      << ", len(retry_token):" << parsed_chlo.retry_token.size() << " }";
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index 93a7e5e..b85e079 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -875,6 +875,7 @@
   std::string sni;                         // QUIC crypto and TLS.
   std::string uaid;                        // QUIC crypto only.
   std::vector<uint16_t> supported_groups;  // TLS only.
+  std::vector<uint16_t> cert_compression_algos;  // TLS only.
   std::vector<std::string> alpns;          // QUIC crypto and TLS.
   // The unvalidated retry token from the last received packet of a potentially
   // multi-packet client hello. TLS only.
diff --git a/quiche/quic/core/tls_chlo_extractor.cc b/quiche/quic/core/tls_chlo_extractor.cc
index 1ba0a20..82beb9f 100644
--- a/quiche/quic/core/tls_chlo_extractor.cc
+++ b/quiche/quic/core/tls_chlo_extractor.cc
@@ -23,6 +23,7 @@
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_versions.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
+#include "quiche/quic/platform/api/quic_flag_utils.h"
 #include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
@@ -71,6 +72,42 @@
   return named_groups;
 }
 
+std::vector<uint16_t> GetCertCompressionAlgos(
+    const SSL_CLIENT_HELLO* client_hello) {
+  const uint8_t* extension_data;
+  size_t extension_len;
+  int rv = SSL_early_callback_ctx_extension_get(
+      client_hello, TLSEXT_TYPE_cert_compression, &extension_data,
+      &extension_len);
+  if (rv != 1) {
+    return {};
+  }
+  // See https://datatracker.ietf.org/doc/html/rfc8879#section-3 for the format
+  // of this extension.
+  QuicDataReader cert_compression_algos_reader(
+      reinterpret_cast<const char*>(extension_data), extension_len);
+  uint8_t algos_len;
+  if (!cert_compression_algos_reader.ReadUInt8(&algos_len) || algos_len == 0 ||
+      algos_len % sizeof(uint16_t) != 0 ||
+      algos_len + sizeof(uint8_t) != extension_len) {
+    QUIC_CODE_COUNT(quic_chlo_cert_compression_algos_invalid_length);
+    return {};
+  }
+
+  size_t num_algos = algos_len / sizeof(uint16_t);
+  std::vector<uint16_t> cert_compression_algos;
+  cert_compression_algos.reserve(num_algos);
+  for (size_t i = 0; i < num_algos; ++i) {
+    uint16_t cert_compression_algo;
+    if (!cert_compression_algos_reader.ReadUInt16(&cert_compression_algo)) {
+      QUIC_CODE_COUNT(quic_chlo_fail_to_read_cert_compression_algo);
+      return {};
+    }
+    cert_compression_algos.push_back(cert_compression_algo);
+  }
+  return cert_compression_algos;
+}
+
 }  // namespace
 
 TlsChloExtractor::TlsChloExtractor()
@@ -102,6 +139,7 @@
   parsed_crypto_frame_in_this_packet_ =
       other.parsed_crypto_frame_in_this_packet_;
   supported_groups_ = std::move(other.supported_groups_);
+  cert_compression_algos_ = std::move(other.cert_compression_algos_);
   alpns_ = std::move(other.alpns_);
   server_name_ = std::move(other.server_name_);
   client_hello_bytes_ = std::move(other.client_hello_bytes_);
@@ -366,6 +404,16 @@
   }
 
   supported_groups_ = GetSupportedGroups(client_hello);
+  if (GetQuicReloadableFlag(quic_parse_cert_compression_algos_from_chlo)) {
+    cert_compression_algos_ = GetCertCompressionAlgos(client_hello);
+    if (cert_compression_algos_.empty()) {
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_parse_cert_compression_algos_from_chlo,
+                                   1, 2);
+    } else {
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_parse_cert_compression_algos_from_chlo,
+                                   2, 2);
+    }
+  }
 
   // Update our state now that we've parsed a full CHLO.
   if (state_ == State::kInitial) {
diff --git a/quiche/quic/core/tls_chlo_extractor.h b/quiche/quic/core/tls_chlo_extractor.h
index 2aaa0eb..8ded967 100644
--- a/quiche/quic/core/tls_chlo_extractor.h
+++ b/quiche/quic/core/tls_chlo_extractor.h
@@ -54,6 +54,9 @@
   const std::vector<uint16_t>& supported_groups() const {
     return supported_groups_;
   }
+  const std::vector<uint16_t>& cert_compression_algos() const {
+    return cert_compression_algos_;
+  }
   absl::Span<const uint8_t> client_hello_bytes() const {
     return client_hello_bytes_;
   }
@@ -263,6 +266,9 @@
   bool parsed_crypto_frame_in_this_packet_;
   // Array of NamedGroups parsed from the CHLO's supported_groups extension.
   std::vector<uint16_t> supported_groups_;
+  // Array of cert compression algos parsed from the CHLO's
+  // compress_certificate extension.
+  std::vector<uint16_t> cert_compression_algos_;
   // Array of ALPNs parsed from the CHLO.
   std::vector<std::string> alpns_;
   // SNI parsed from the CHLO.
diff --git a/quiche/quic/core/tls_chlo_extractor_test.cc b/quiche/quic/core/tls_chlo_extractor_test.cc
index 238d7b4..2bc2044 100644
--- a/quiche/quic/core/tls_chlo_extractor_test.cc
+++ b/quiche/quic/core/tls_chlo_extractor_test.cc
@@ -15,16 +15,29 @@
 #include "quiche/quic/core/quic_connection.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/core/quic_versions.h"
+#include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_test.h"
 #include "quiche/quic/test_tools/crypto_test_utils.h"
 #include "quiche/quic/test_tools/first_flight.h"
 #include "quiche/quic/test_tools/quic_test_utils.h"
 #include "quiche/quic/test_tools/simple_session_cache.h"
+#include "quiche/common/print_elements.h"
 
 namespace quic {
 namespace test {
 namespace {
 
+static int DummyCompressFunc(SSL* /*ssl*/, CBB* /*out*/, const uint8_t* /*in*/,
+                             size_t /*in_len*/) {
+  return 1;
+}
+
+static int DummyDecompressFunc(SSL* /*ssl*/, CRYPTO_BUFFER** /*out*/,
+                               size_t /*uncompressed_len*/,
+                               const uint8_t* /*in*/, size_t /*in_len*/) {
+  return 1;
+}
+
 using testing::_;
 using testing::AnyNumber;
 
@@ -234,6 +247,43 @@
   }
 }
 
+TEST_P(TlsChloExtractorTest, TlsExtensionInfo_CertCompressionAlgos) {
+  const std::vector<std::vector<uint16_t>> supported_groups_to_test = {
+      // No cert compression algos
+      {},
+      // One cert compression algo
+      {1},
+      // Two cert compression algos
+      {1, 2},
+      // Three cert compression algos
+      {1, 2, 3},
+      // Four cert compression algos
+      {1, 2, 3, 65535},
+  };
+  for (const std::vector<uint16_t>& supported_cert_compression_algos :
+       supported_groups_to_test) {
+    auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>(
+        crypto_test_utils::ProofVerifierForTesting());
+    for (uint16_t cert_compression_algo : supported_cert_compression_algos) {
+      ASSERT_TRUE(SSL_CTX_add_cert_compression_alg(
+          crypto_client_config->ssl_ctx(), cert_compression_algo,
+          DummyCompressFunc, DummyDecompressFunc));
+    }
+
+    Initialize(std::move(crypto_client_config));
+    IngestPackets();
+    ValidateChloDetails();
+    if (GetQuicReloadableFlag(quic_parse_cert_compression_algos_from_chlo)) {
+      EXPECT_EQ(tls_chlo_extractor_->cert_compression_algos(),
+                supported_cert_compression_algos)
+          << quiche::PrintElements(
+                 tls_chlo_extractor_->cert_compression_algos());
+    } else {
+      EXPECT_TRUE(tls_chlo_extractor_->cert_compression_algos().empty());
+    }
+  }
+}
+
 TEST_P(TlsChloExtractorTest, MultiPacket) {
   IncreaseSizeOfChlo();
   Initialize();