Add support for draft-ietf-quic-version-negotiation-05

This CL adds support for reading and writing version_information transport parameter, and validates it when received. The server sending and receipt are protected by gfe2_reloadable_flag_quic_version_information.

Protected by FLAGS_quic_reloadable_flag_quic_version_information.

PiperOrigin-RevId: 408865522
diff --git a/quic/core/crypto/crypto_utils.cc b/quic/core/crypto/crypto_utils.cc
index 2f1fa34..cc479ab 100644
--- a/quic/core/crypto/crypto_utils.cc
+++ b/quic/core/crypto/crypto_utils.cc
@@ -641,6 +641,62 @@
   return QUIC_NO_ERROR;
 }
 
+// static
+bool CryptoUtils::ValidateChosenVersion(
+    const QuicVersionLabel& version_information_chosen_version,
+    const ParsedQuicVersion& session_version, std::string* error_details) {
+  if (version_information_chosen_version !=
+      CreateQuicVersionLabel(session_version)) {
+    *error_details = absl::StrCat(
+        "Detected version mismatch: version_information contained ",
+        QuicVersionLabelToString(version_information_chosen_version),
+        " instead of ", ParsedQuicVersionToString(session_version));
+    return false;
+  }
+  return true;
+}
+
+// static
+bool CryptoUtils::ValidateServerVersions(
+    const QuicVersionLabelVector& version_information_other_versions,
+    const ParsedQuicVersion& session_version,
+    const ParsedQuicVersionVector& client_original_supported_versions,
+    std::string* error_details) {
+  if (client_original_supported_versions.empty()) {
+    // We did not receive a version negotiation packet.
+    return true;
+  }
+  // Parse the server's other versions.
+  ParsedQuicVersionVector parsed_other_versions =
+      ParseQuicVersionLabelVector(version_information_other_versions);
+  // Find the first version that we originally supported that is listed in the
+  // server's other versions.
+  ParsedQuicVersion expected_version = ParsedQuicVersion::Unsupported();
+  for (const ParsedQuicVersion& client_version :
+       client_original_supported_versions) {
+    if (std::find(parsed_other_versions.begin(), parsed_other_versions.end(),
+                  client_version) != parsed_other_versions.end()) {
+      expected_version = client_version;
+      break;
+    }
+  }
+  if (expected_version != session_version) {
+    *error_details = absl::StrCat(
+        "Downgrade attack detected: used ",
+        ParsedQuicVersionToString(session_version), " but ServerVersions(",
+        version_information_other_versions.size(), ")[",
+        QuicVersionLabelVectorToString(version_information_other_versions, ",",
+                                       30),
+        "] ClientOriginalVersions(", client_original_supported_versions.size(),
+        ")[",
+        ParsedQuicVersionVectorToString(client_original_supported_versions, ",",
+                                        30),
+        "]");
+    return false;
+  }
+  return true;
+}
+
 #define RETURN_STRING_LITERAL(x) \
   case x:                        \
     return #x
diff --git a/quic/core/crypto/crypto_utils.h b/quic/core/crypto/crypto_utils.h
index 15ffe06..8884f6f 100644
--- a/quic/core/crypto/crypto_utils.h
+++ b/quic/core/crypto/crypto_utils.h
@@ -217,6 +217,27 @@
       const ParsedQuicVersionVector& supported_versions,
       std::string* error_details);
 
+  // Validates that the chosen version from the version_information matches the
+  // version from the session. Returns true if they match, otherwise returns
+  // false and fills in |error_details|.
+  static bool ValidateChosenVersion(
+      const QuicVersionLabel& version_information_chosen_version,
+      const ParsedQuicVersion& session_version, std::string* error_details);
+
+  // Validates that there was no downgrade attack involving a version
+  // negotiation packet. This verifies that if the client was initially
+  // configured with |client_original_supported_versions| and it had received a
+  // version negotiation packet with |version_information_other_versions|, then
+  // it would have selected |session_version|. Returns true if they match (or if
+  // |client_original_supported_versions| is empty indicating no version
+  // negotiation packet was received), otherwise returns
+  // false and fills in |error_details|.
+  static bool ValidateServerVersions(
+      const QuicVersionLabelVector& version_information_other_versions,
+      const ParsedQuicVersion& session_version,
+      const ParsedQuicVersionVector& client_original_supported_versions,
+      std::string* error_details);
+
   // Returns the name of the HandshakeFailureReason as a char*
   static const char* HandshakeFailureReasonToString(
       HandshakeFailureReason reason);
diff --git a/quic/core/crypto/crypto_utils_test.cc b/quic/core/crypto/crypto_utils_test.cc
index 6c3d384..251f136 100644
--- a/quic/core/crypto/crypto_utils_test.cc
+++ b/quic/core/crypto/crypto_utils_test.cc
@@ -108,6 +108,63 @@
   }
 }
 
+TEST_F(CryptoUtilsTest, ValidateChosenVersion) {
+  for (const ParsedQuicVersion& v1 : AllSupportedVersions()) {
+    for (const ParsedQuicVersion& v2 : AllSupportedVersions()) {
+      std::string error_details;
+      bool success = CryptoUtils::ValidateChosenVersion(
+          CreateQuicVersionLabel(v1), v2, &error_details);
+      EXPECT_EQ(success, v1 == v2);
+      EXPECT_EQ(success, error_details.empty());
+    }
+  }
+}
+
+TEST_F(CryptoUtilsTest, ValidateServerVersionsNoVersionNegotiation) {
+  QuicVersionLabelVector version_information_other_versions;
+  ParsedQuicVersionVector client_original_supported_versions;
+  for (const ParsedQuicVersion& version : AllSupportedVersions()) {
+    std::string error_details;
+    EXPECT_TRUE(CryptoUtils::ValidateServerVersions(
+        version_information_other_versions, version,
+        client_original_supported_versions, &error_details));
+    EXPECT_TRUE(error_details.empty());
+  }
+}
+
+TEST_F(CryptoUtilsTest, ValidateServerVersionsWithVersionNegotiation) {
+  for (const ParsedQuicVersion& version : AllSupportedVersions()) {
+    QuicVersionLabelVector version_information_other_versions{
+        CreateQuicVersionLabel(version)};
+    ParsedQuicVersionVector client_original_supported_versions{
+        ParsedQuicVersion::ReservedForNegotiation(), version};
+    std::string error_details;
+    EXPECT_TRUE(CryptoUtils::ValidateServerVersions(
+        version_information_other_versions, version,
+        client_original_supported_versions, &error_details));
+    EXPECT_TRUE(error_details.empty());
+  }
+}
+
+TEST_F(CryptoUtilsTest, ValidateServerVersionsWithDowngrade) {
+  if (AllSupportedVersions().size() <= 1) {
+    // We are not vulnerable to downgrade if we only support one version.
+    return;
+  }
+  ParsedQuicVersion client_version = AllSupportedVersions().front();
+  ParsedQuicVersion server_version = AllSupportedVersions().back();
+  ASSERT_NE(client_version, server_version);
+  QuicVersionLabelVector version_information_other_versions{
+      CreateQuicVersionLabel(client_version)};
+  ParsedQuicVersionVector client_original_supported_versions{
+      ParsedQuicVersion::ReservedForNegotiation(), server_version};
+  std::string error_details;
+  EXPECT_FALSE(CryptoUtils::ValidateServerVersions(
+      version_information_other_versions, server_version,
+      client_original_supported_versions, &error_details));
+  EXPECT_FALSE(error_details.empty());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/crypto/transport_parameters.cc b/quic/core/crypto/transport_parameters.cc
index 1ad7818..61e8f7c 100644
--- a/quic/core/crypto/transport_parameters.cc
+++ b/quic/core/crypto/transport_parameters.cc
@@ -62,7 +62,8 @@
   kGoogleQuicVersion =
       0x4752,  // Used to transmit version and supported_versions.
 
-  kMinAckDelay = 0xDE1A,  // draft-iyengar-quic-delayed-ack.
+  kMinAckDelay = 0xDE1A,           // draft-iyengar-quic-delayed-ack.
+  kVersionInformation = 0xFF73DB,  // draft-ietf-quic-version-negotiation.
 };
 
 namespace {
@@ -129,6 +130,8 @@
       return "google-version";
     case TransportParameters::kMinAckDelay:
       return "min_ack_delay_us";
+    case TransportParameters::kVersionInformation:
+      return "version_information";
   }
   return absl::StrCat("Unknown(", param_id, ")");
 }
@@ -159,6 +162,8 @@
     case TransportParameters::kGoogleQuicVersion:
     case TransportParameters::kMinAckDelay:
       return true;
+    case TransportParameters::kVersionInformation:
+      return GetQuicReloadableFlag(quic_version_information);
     case TransportParameters::kGoogleUserAgentId:
       return !GetQuicReloadableFlag(quic_ignore_user_agent_transport_parameter);
     case TransportParameters::kGoogleKeyUpdateNotYetSupported:
@@ -341,6 +346,38 @@
   return os;
 }
 
+TransportParameters::VersionInformation::VersionInformation()
+    : chosen_version(0) {}
+
+bool TransportParameters::VersionInformation::operator==(
+    const VersionInformation& rhs) const {
+  return chosen_version == rhs.chosen_version &&
+         other_versions == rhs.other_versions;
+}
+
+bool TransportParameters::VersionInformation::operator!=(
+    const VersionInformation& rhs) const {
+  return !(*this == rhs);
+}
+
+std::string TransportParameters::VersionInformation::ToString() const {
+  std::string rv = absl::StrCat("[chosen_version ",
+                                QuicVersionLabelToString(chosen_version));
+  if (!other_versions.empty()) {
+    absl::StrAppend(&rv, " other_versions " +
+                             QuicVersionLabelVectorToString(other_versions));
+  }
+  absl::StrAppend(&rv, "]");
+  return rv;
+}
+
+std::ostream& operator<<(
+    std::ostream& os,
+    const TransportParameters::VersionInformation& version_information) {
+  os << version_information.ToString();
+  return os;
+}
+
 std::ostream& operator<<(std::ostream& os, const TransportParameters& params) {
   os << params.ToString();
   return os;
@@ -356,6 +393,9 @@
   if (legacy_version_information.has_value()) {
     rv += " " + legacy_version_information.value().ToString();
   }
+  if (version_information.has_value()) {
+    rv += " " + version_information.value().ToString();
+  }
   if (original_destination_connection_id.has_value()) {
     rv += " " + TransportParameterIdToString(kOriginalDestinationConnectionId) +
           " " + original_destination_connection_id.value().ToString();
@@ -464,6 +504,7 @@
 TransportParameters::TransportParameters(const TransportParameters& other)
     : perspective(other.perspective),
       legacy_version_information(other.legacy_version_information),
+      version_information(other.version_information),
       original_destination_connection_id(
           other.original_destination_connection_id),
       max_idle_timeout_ms(other.max_idle_timeout_ms),
@@ -499,6 +540,7 @@
 bool TransportParameters::operator==(const TransportParameters& rhs) const {
   if (!(perspective == rhs.perspective &&
         legacy_version_information == rhs.legacy_version_information &&
+        version_information == rhs.version_information &&
         original_destination_connection_id ==
             rhs.original_destination_connection_id &&
         max_idle_timeout_ms.value() == rhs.max_idle_timeout_ms.value() &&
@@ -608,6 +650,29 @@
     *error_details = "Server cannot send user agent ID";
     return false;
   }
+  if (version_information.has_value()) {
+    const QuicVersionLabel& chosen_version =
+        version_information.value().chosen_version;
+    const QuicVersionLabelVector& other_versions =
+        version_information.value().other_versions;
+    if (chosen_version == 0) {
+      *error_details = "Invalid chosen version";
+      return false;
+    }
+    if (perspective == Perspective::IS_CLIENT &&
+        std::find(other_versions.begin(), other_versions.end(),
+                  chosen_version) == other_versions.end()) {
+      // When sent by the client, chosen_version needs to be present in
+      // other_versions because other_versions lists the compatible versions and
+      // the chosen version is part of that list. When sent by the server,
+      // other_version contains the list of fully-deployed versions which is
+      // generally equal to the list of supported versions but can slightly
+      // differ during removal of versions across a server fleet. See
+      // draft-ietf-quic-version-negotiation for details.
+      *error_details = "Client chosen version not in other versions";
+      return false;
+    }
+  }
   const bool ok =
       max_idle_timeout_ms.IsValid() && max_udp_payload_size.IsValid() &&
       initial_max_data.IsValid() &&
@@ -722,6 +787,7 @@
       TransportParameters::kGoogleUserAgentId,
       TransportParameters::kGoogleKeyUpdateNotYetSupported,
       TransportParameters::kGoogleQuicVersion,
+      TransportParameters::kVersionInformation,
   };
 
   size_t max_transport_param_length = kKnownTransportParamLength;
@@ -742,6 +808,14 @@
         in.legacy_version_information.value().supported_versions.size() *
             sizeof(QuicVersionLabel);
   }
+  // version_information.
+  if (in.version_information.has_value()) {
+    max_transport_param_length +=
+        sizeof(in.version_information.value().chosen_version) +
+        // Add one for the added GREASE version.
+        (in.version_information.value().other_versions.size() + 1) *
+            sizeof(QuicVersionLabel);
+  }
 
   // Add a random GREASE transport parameter, as defined in the
   // "Reserved Transport Parameters" section of RFC 9000.
@@ -1116,6 +1190,41 @@
           }
         }
       } break;
+      // version_information.
+      case TransportParameters::kVersionInformation: {
+        if (!in.version_information.has_value()) {
+          break;
+        }
+        static_assert(sizeof(QuicVersionLabel) == sizeof(uint32_t),
+                      "bad length");
+        QuicVersionLabelVector other_versions =
+            in.version_information.value().other_versions;
+        // Insert one GREASE version at a random index.
+        const size_t grease_index =
+            random->InsecureRandUint64() % (other_versions.size() + 1);
+        other_versions.insert(
+            other_versions.begin() + grease_index,
+            CreateQuicVersionLabel(QuicVersionReservedForNegotiation()));
+        const uint64_t version_information_length =
+            sizeof(in.version_information.value().chosen_version) +
+            sizeof(QuicVersionLabel) * other_versions.size();
+        if (!writer.WriteVarInt62(TransportParameters::kVersionInformation) ||
+            !writer.WriteVarInt62(
+                /* transport parameter length */ version_information_length) ||
+            !writer.WriteUInt32(
+                in.version_information.value().chosen_version)) {
+          QUIC_BUG(Failed to write chosen version)
+              << "Failed to write chosen version for " << in;
+          return false;
+        }
+        for (QuicVersionLabel version_label : other_versions) {
+          if (!writer.WriteUInt32(version_label)) {
+            QUIC_BUG(Failed to write other version)
+                << "Failed to write other version for " << in;
+            return false;
+          }
+        }
+      } break;
       // Custom parameters and GREASE.
       default: {
         auto it = custom_parameters.find(parameter_id);
@@ -1437,6 +1546,41 @@
           }
         }
       } break;
+      case TransportParameters::kVersionInformation: {
+        if (!GetQuicReloadableFlag(quic_version_information)) {
+          // This duplicates the default case and will be removed when this flag
+          // is deprecated.
+          if (out->custom_parameters.find(param_id) !=
+              out->custom_parameters.end()) {
+            *error_details = "Received a second unknown parameter" +
+                             TransportParameterIdToString(param_id);
+            return false;
+          }
+          out->custom_parameters[param_id] =
+              std::string(value_reader.ReadRemainingPayload());
+          break;
+        }
+        QUIC_RELOADABLE_FLAG_COUNT_N(quic_version_information, 2, 2);
+        if (out->version_information.has_value()) {
+          *error_details = "Received a second version_information";
+          return false;
+        }
+        out->version_information = TransportParameters::VersionInformation();
+        if (!value_reader.ReadUInt32(
+                &out->version_information.value().chosen_version)) {
+          *error_details = "Failed to read chosen version";
+          return false;
+        }
+        while (!value_reader.IsDoneReading()) {
+          QuicVersionLabel other_version;
+          if (!value_reader.ReadUInt32(&other_version)) {
+            *error_details = "Failed to parse other version";
+            return false;
+          }
+          out->version_information.value().other_versions.push_back(
+              other_version);
+        }
+      } break;
       case TransportParameters::kMinAckDelay:
         parse_success =
             out->min_ack_delay_us.Read(&value_reader, error_details);
diff --git a/quic/core/crypto/transport_parameters.h b/quic/core/crypto/transport_parameters.h
index 1d66ee9..fa34935 100644
--- a/quic/core/crypto/transport_parameters.h
+++ b/quic/core/crypto/transport_parameters.h
@@ -142,6 +142,32 @@
         const LegacyVersionInformation& legacy_version_information);
   };
 
+  // Version information used for version downgrade prevention and compatible
+  // version negotiation. See draft-ietf-quic-version-negotiation-05.
+  struct QUIC_EXPORT_PRIVATE VersionInformation {
+    VersionInformation();
+    VersionInformation(const VersionInformation& other) = default;
+    VersionInformation& operator=(const VersionInformation& other) = default;
+    VersionInformation& operator=(VersionInformation&& other) = default;
+    VersionInformation(VersionInformation&& other) = default;
+    ~VersionInformation() = default;
+    bool operator==(const VersionInformation& rhs) const;
+    bool operator!=(const VersionInformation& rhs) const;
+
+    // Version that the sender has chosen to use on this connection.
+    QuicVersionLabel chosen_version;
+
+    // When sent by the client, |other_versions| contains all the versions that
+    // this first flight is compatible with. When sent by the server,
+    // |other_versions| contains all of the versions supported by the server.
+    QuicVersionLabelVector other_versions;
+
+    // Allows easily logging.
+    std::string ToString() const;
+    friend QUIC_EXPORT_PRIVATE std::ostream& operator<<(
+        std::ostream& os, const VersionInformation& version_information);
+  };
+
   TransportParameters();
   TransportParameters(const TransportParameters& other);
   ~TransportParameters();
@@ -157,6 +183,10 @@
   // Google QUIC downgrade prevention mechanism sent over QUIC+TLS.
   absl::optional<LegacyVersionInformation> legacy_version_information;
 
+  // IETF downgrade prevention and compatible version negotiation, see
+  // draft-ietf-quic-version-negotiation.
+  absl::optional<VersionInformation> version_information;
+
   // The value of the Destination Connection ID field from the first
   // Initial packet sent by the client.
   absl::optional<QuicConnectionId> original_destination_connection_id;
diff --git a/quic/core/crypto/transport_parameters_test.cc b/quic/core/crypto/transport_parameters_test.cc
index 4ba0500..d9b5573 100644
--- a/quic/core/crypto/transport_parameters_test.cc
+++ b/quic/core/crypto/transport_parameters_test.cc
@@ -115,6 +115,14 @@
   return legacy_version_information;
 }
 
+TransportParameters::VersionInformation CreateFakeVersionInformation() {
+  TransportParameters::VersionInformation version_information;
+  version_information.chosen_version = kFakeVersionLabel;
+  version_information.other_versions.push_back(kFakeVersionLabel);
+  version_information.other_versions.push_back(kFakeVersionLabel2);
+  return version_information;
+}
+
 QuicTagVector CreateFakeGoogleConnectionOptions() {
   return {kALPN, MakeQuicTag('E', 'F', 'G', 0x00),
           MakeQuicTag('H', 'I', 'J', 0xff)};
@@ -135,6 +143,18 @@
   for (TransportParameters::TransportParameterId param_id : grease_params) {
     params->custom_parameters.erase(param_id);
   }
+  // Remove all GREASE versions from version_information.other_versions.
+  if (params->version_information.has_value()) {
+    QuicVersionLabelVector& other_versions =
+        params->version_information.value().other_versions;
+    for (auto it = other_versions.begin(); it != other_versions.end();) {
+      if ((*it & 0x0f0f0f0f) == 0x0a0a0a0a) {
+        it = other_versions.erase(it);
+      } else {
+        ++it;
+      }
+    }
+  }
 }
 
 }  // namespace
@@ -165,6 +185,8 @@
       CreateFakeLegacyVersionInformationClient();
   new_params.legacy_version_information =
       CreateFakeLegacyVersionInformationClient();
+  orig_params.version_information = CreateFakeVersionInformation();
+  new_params.version_information = CreateFakeVersionInformation();
   orig_params.disable_active_migration = true;
   new_params.disable_active_migration = true;
   EXPECT_EQ(orig_params, new_params);
@@ -242,6 +264,7 @@
   orig_params.perspective = Perspective::IS_CLIENT;
   orig_params.legacy_version_information =
       CreateFakeLegacyVersionInformationClient();
+  orig_params.version_information = CreateFakeVersionInformation();
   orig_params.original_destination_connection_id =
       CreateFakeOriginalDestinationConnectionId();
   orig_params.max_idle_timeout_ms.set_value(kFakeIdleTimeoutMilliseconds);
@@ -282,6 +305,9 @@
   orig_params.perspective = Perspective::IS_CLIENT;
   orig_params.legacy_version_information =
       CreateFakeLegacyVersionInformationClient();
+  if (GetQuicReloadableFlag(quic_version_information)) {
+    orig_params.version_information = CreateFakeVersionInformation();
+  }
   orig_params.max_idle_timeout_ms.set_value(kFakeIdleTimeoutMilliseconds);
   orig_params.max_udp_payload_size.set_value(kMaxPacketSizeForTest);
   orig_params.initial_max_data.set_value(kFakeInitialMaxData);
@@ -331,6 +357,9 @@
   orig_params.perspective = Perspective::IS_SERVER;
   orig_params.legacy_version_information =
       CreateFakeLegacyVersionInformationServer();
+  if (GetQuicReloadableFlag(quic_version_information)) {
+    orig_params.version_information = CreateFakeVersionInformation();
+  }
   orig_params.original_destination_connection_id =
       CreateFakeOriginalDestinationConnectionId();
   orig_params.max_idle_timeout_ms.set_value(kFakeIdleTimeoutMilliseconds);
@@ -560,6 +589,12 @@
       0x80, 0x00, 0x47, 0x52,  // parameter id
       0x04,  // length
       0x01, 0x23, 0x45, 0x67,  // initial version
+      // version_information
+      0x80, 0xFF, 0x73, 0xDB,  // parameter id
+      0x0C,  // length
+      0x01, 0x23, 0x45, 0x67,  // chosen version
+      0x01, 0x23, 0x45, 0x67,  // other version 1
+      0x89, 0xab, 0xcd, 0xef,  // other version 2
   };
   // clang-format on
   const uint8_t* client_params =
@@ -578,6 +613,11 @@
             new_params.legacy_version_information.value().version);
   EXPECT_TRUE(
       new_params.legacy_version_information.value().supported_versions.empty());
+  if (GetQuicReloadableFlag(quic_version_information)) {
+    ASSERT_TRUE(new_params.version_information.has_value());
+    EXPECT_EQ(new_params.version_information.value(),
+              CreateFakeVersionInformation());
+  }
   EXPECT_FALSE(new_params.original_destination_connection_id.has_value());
   EXPECT_EQ(kFakeIdleTimeoutMilliseconds,
             new_params.max_idle_timeout_ms.value());
@@ -814,6 +854,12 @@
       0x08,  // length of supported versions array
       0x01, 0x23, 0x45, 0x67,
       0x89, 0xab, 0xcd, 0xef,
+      // version_information
+      0x80, 0xFF, 0x73, 0xDB,  // parameter id
+      0x0C,  // length
+      0x01, 0x23, 0x45, 0x67,  // chosen version
+      0x01, 0x23, 0x45, 0x67,  // other version 1
+      0x89, 0xab, 0xcd, 0xef,  // other version 2
   };
   // clang-format on
   const uint8_t* server_params =
@@ -839,6 +885,11 @@
   EXPECT_EQ(
       kFakeVersionLabel2,
       new_params.legacy_version_information.value().supported_versions[1]);
+  if (GetQuicReloadableFlag(quic_version_information)) {
+    ASSERT_TRUE(new_params.version_information.has_value());
+    EXPECT_EQ(new_params.version_information.value(),
+              CreateFakeVersionInformation());
+  }
   ASSERT_TRUE(new_params.original_destination_connection_id.has_value());
   EXPECT_EQ(CreateFakeOriginalDestinationConnectionId(),
             new_params.original_destination_connection_id.value());
diff --git a/quic/core/http/end_to_end_test.cc b/quic/core/http/end_to_end_test.cc
index 949e703..c9d5291 100644
--- a/quic/core/http/end_to_end_test.cc
+++ b/quic/core/http/end_to_end_test.cc
@@ -3964,6 +3964,112 @@
   client_connection->set_debug_visitor(nullptr);
 }
 
+// DowngradePacketWriter is a client writer which will intercept all the client
+// writes for |target_version| and reply to them with version negotiation
+// packets to attempt a version downgrade attack. Once the client has downgraded
+// to a different version, the writer stops intercepting. |server_thread| must
+// start off paused, and will be resumed once interception is done.
+class DowngradePacketWriter : public PacketDroppingTestWriter {
+ public:
+  explicit DowngradePacketWriter(
+      const ParsedQuicVersion& target_version,
+      const ParsedQuicVersionVector& supported_versions, QuicTestClient* client,
+      QuicPacketWriter* server_writer, ServerThread* server_thread)
+      : target_version_(target_version),
+        supported_versions_(supported_versions),
+        client_(client),
+        server_writer_(server_writer),
+        server_thread_(server_thread) {}
+  ~DowngradePacketWriter() override {}
+
+  WriteResult WritePacket(const char* buffer, size_t buf_len,
+                          const QuicIpAddress& self_address,
+                          const QuicSocketAddress& peer_address,
+                          quic::PerPacketOptions* options) override {
+    if (!intercept_enabled_) {
+      return PacketDroppingTestWriter::WritePacket(
+          buffer, buf_len, self_address, peer_address, options);
+    }
+    PacketHeaderFormat format;
+    QuicLongHeaderType long_packet_type;
+    bool version_present, has_length_prefix;
+    QuicVersionLabel version_label;
+    ParsedQuicVersion parsed_version = ParsedQuicVersion::Unsupported();
+    QuicConnectionId destination_connection_id, source_connection_id;
+    absl::optional<absl::string_view> retry_token;
+    std::string detailed_error;
+    if (QuicFramer::ParsePublicHeaderDispatcher(
+            QuicEncryptedPacket(buffer, buf_len),
+            kQuicDefaultConnectionIdLength, &format, &long_packet_type,
+            &version_present, &has_length_prefix, &version_label,
+            &parsed_version, &destination_connection_id, &source_connection_id,
+            &retry_token, &detailed_error) != QUIC_NO_ERROR) {
+      ADD_FAILURE() << "Failed to parse our own packet: " << detailed_error;
+      return WriteResult(WRITE_STATUS_ERROR, 0);
+    }
+    if (!version_present || parsed_version != target_version_) {
+      // Client is sending with another version, the attack has succeeded so we
+      // can stop intercepting.
+      intercept_enabled_ = false;
+      server_thread_->Resume();
+      // Pass the client-sent packet through.
+      return WritePacket(buffer, buf_len, self_address, peer_address, options);
+    }
+    // Send a version negotiation packet.
+    std::unique_ptr<QuicEncryptedPacket> packet(
+        QuicFramer::BuildVersionNegotiationPacket(
+            destination_connection_id, source_connection_id,
+            parsed_version.HasIetfInvariantHeader(), has_length_prefix,
+            supported_versions_));
+    server_writer_->WritePacket(
+        packet->data(), packet->length(), peer_address.host(),
+        client_->client()->network_helper()->GetLatestClientAddress(), nullptr);
+    // Drop the client-sent packet but pretend it was sent.
+    return WriteResult(WRITE_STATUS_OK, buf_len);
+  }
+
+ private:
+  bool intercept_enabled_ = true;
+  ParsedQuicVersion target_version_;
+  ParsedQuicVersionVector supported_versions_;
+  QuicTestClient* client_;           // Unowned.
+  QuicPacketWriter* server_writer_;  // Unowned.
+  ServerThread* server_thread_;      // Unowned.
+};
+
+TEST_P(EndToEndTest, VersionNegotiationDowngradeAttackIsDetected) {
+  ParsedQuicVersion target_version = server_supported_versions_.back();
+  if (!version_.UsesTls() || target_version == version_) {
+    ASSERT_TRUE(Initialize());
+    return;
+  }
+  SetQuicReloadableFlag(quic_version_information, true);
+  connect_to_server_on_initialize_ = false;
+  client_supported_versions_.insert(client_supported_versions_.begin(),
+                                    target_version);
+  ParsedQuicVersionVector downgrade_versions{version_};
+  ASSERT_TRUE(Initialize());
+  ASSERT_TRUE(server_thread_);
+  // Pause the server thread to allow our DowngradePacketWriter to write version
+  // negotiation packets in a thread-safe manner. It will be resumed by the
+  // DowngradePacketWriter.
+  server_thread_->Pause();
+  client_.reset(new QuicTestClient(server_address_, server_hostname_,
+                                   client_config_, client_supported_versions_,
+                                   crypto_test_utils::ProofVerifierForTesting(),
+                                   std::make_unique<SimpleSessionCache>()));
+  delete client_writer_;
+  client_writer_ = new DowngradePacketWriter(target_version, downgrade_versions,
+                                             client_.get(), server_writer_,
+                                             server_thread_.get());
+  client_->UseWriter(client_writer_);
+  // Have the client attempt to send a request.
+  client_->Connect();
+  EXPECT_TRUE(client_->SendSynchronousRequest("/foo").empty());
+  // Make sure the downgrade is detected and the handshake fails.
+  EXPECT_THAT(client_->connection_error(), IsError(QUIC_HANDSHAKE_FAILED));
+}
+
 // A bad header shouldn't tear down the connection, because the receiver can't
 // tell the connection ID.
 TEST_P(EndToEndTest, BadPacketHeaderTruncated) {
diff --git a/quic/core/quic_flags_list.h b/quic/core/quic_flags_list.h
index d625fd0..0b5e591 100644
--- a/quic/core/quic_flags_list.h
+++ b/quic/core/quic_flags_list.h
@@ -133,6 +133,8 @@
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_ignore_key_update_not_yet_supported, true)
 // When true, QUIC server will ignore received user agent transport parameter and rely on getting that information from HTTP headers.
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_ignore_user_agent_transport_parameter, true)
+// When true, QUIC will both send and validate the version_information transport parameter.
+QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_version_information, false)
 // When true, QuicDispatcher will silently drop incoming packets whose UDP source port is on the blocklist.
 QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_blocked_ports, true)
 // When true, defaults to BBR congestion control instead of Cubic.
diff --git a/quic/core/quic_session.h b/quic/core/quic_session.h
index cb06d87..db898d2 100644
--- a/quic/core/quic_session.h
+++ b/quic/core/quic_session.h
@@ -630,6 +630,16 @@
   // Try converting all pending streams to normal streams.
   void ProcessAllPendingStreams();
 
+  const ParsedQuicVersionVector& client_original_supported_versions() const {
+    QUICHE_DCHECK_EQ(perspective_, Perspective::IS_CLIENT);
+    return client_original_supported_versions_;
+  }
+  void set_client_original_supported_versions(
+      const ParsedQuicVersionVector& client_original_supported_versions) {
+    QUICHE_DCHECK_EQ(perspective_, Perspective::IS_CLIENT);
+    client_original_supported_versions_ = client_original_supported_versions;
+  }
+
  protected:
   using StreamMap =
       absl::flat_hash_map<QuicStreamId, std::unique_ptr<QuicStream>>;
@@ -983,6 +993,11 @@
   // list may be a superset of the connection framer's supported versions.
   ParsedQuicVersionVector supported_versions_;
 
+  // Only non-empty on the client after receiving a version negotiation packet,
+  // contains the configured versions from the original session before version
+  // negotiation was received.
+  ParsedQuicVersionVector client_original_supported_versions_;
+
   absl::optional<std::string> user_agent_id_;
 
   // Initialized to false. Set to true when the session has been properly
diff --git a/quic/core/quic_versions.cc b/quic/core/quic_versions.cc
index 717932f..cff0b92 100644
--- a/quic/core/quic_versions.cc
+++ b/quic/core/quic_versions.cc
@@ -321,6 +321,18 @@
   return UnsupportedQuicVersion();
 }
 
+ParsedQuicVersionVector ParseQuicVersionLabelVector(
+    const QuicVersionLabelVector& version_labels) {
+  ParsedQuicVersionVector parsed_versions;
+  for (const QuicVersionLabel& version_label : version_labels) {
+    ParsedQuicVersion parsed_version = ParseQuicVersionLabel(version_label);
+    if (parsed_version.IsKnown()) {
+      parsed_versions.push_back(parsed_version);
+    }
+  }
+  return parsed_versions;
+}
+
 ParsedQuicVersion ParseQuicVersionString(absl::string_view version_string) {
   if (version_string.empty()) {
     return UnsupportedQuicVersion();
@@ -611,6 +623,7 @@
 
 void QuicVersionInitializeSupportForIetfDraft() {
   // Enable necessary flags.
+  SetQuicReloadableFlag(quic_version_information, true);
 }
 
 void QuicEnableVersion(const ParsedQuicVersion& version) {
diff --git a/quic/core/quic_versions.h b/quic/core/quic_versions.h
index 52e829c..695258e 100644
--- a/quic/core/quic_versions.h
+++ b/quic/core/quic_versions.h
@@ -462,6 +462,11 @@
 QUIC_EXPORT_PRIVATE ParsedQuicVersion
 ParseQuicVersionLabel(QuicVersionLabel version_label);
 
+// Helper function that translates from a QuicVersionLabelVector to a
+// ParsedQuicVersionVector.
+QUIC_EXPORT_PRIVATE ParsedQuicVersionVector
+ParseQuicVersionLabelVector(const QuicVersionLabelVector& version_labels);
+
 // Parses a QUIC version string such as "Q043" or "T051". Also supports parsing
 // ALPN such as "h3-29" or "h3-Q050". For PROTOCOL_QUIC_CRYPTO versions, also
 // supports parsing numbers such as "46".
diff --git a/quic/core/quic_versions_test.cc b/quic/core/quic_versions_test.cc
index 31e365e..0ee3b4f 100644
--- a/quic/core/quic_versions_test.cc
+++ b/quic/core/quic_versions_test.cc
@@ -121,6 +121,14 @@
             ParseQuicVersionLabel(MakeVersionLabel('T', '0', '5', '1')));
   EXPECT_EQ(ParsedQuicVersion::Draft29(),
             ParseQuicVersionLabel(MakeVersionLabel(0xff, 0x00, 0x00, 0x1d)));
+  EXPECT_EQ(ParsedQuicVersion::RFCv1(),
+            ParseQuicVersionLabel(MakeVersionLabel(0x00, 0x00, 0x00, 0x01)));
+  EXPECT_EQ((ParsedQuicVersionVector{ParsedQuicVersion::RFCv1(),
+                                     ParsedQuicVersion::Draft29()}),
+            ParseQuicVersionLabelVector(QuicVersionLabelVector{
+                MakeVersionLabel(0x00, 0x00, 0x00, 0x01),
+                MakeVersionLabel(0xaa, 0xaa, 0xaa, 0xaa),
+                MakeVersionLabel(0xff, 0x00, 0x00, 0x1d)}));
 }
 
 TEST_F(QuicVersionsTest, ParseQuicVersionString) {
diff --git a/quic/core/tls_client_handshaker.cc b/quic/core/tls_client_handshaker.cc
index b0bd516..7f8fca6 100644
--- a/quic/core/tls_client_handshaker.cc
+++ b/quic/core/tls_client_handshaker.cc
@@ -225,6 +225,10 @@
       TransportParameters::LegacyVersionInformation();
   params.legacy_version_information.value().version =
       CreateQuicVersionLabel(session()->supported_versions().front());
+  params.version_information = TransportParameters::VersionInformation();
+  const QuicVersionLabel version = CreateQuicVersionLabel(session()->version());
+  params.version_information.value().chosen_version = version;
+  params.version_information.value().other_versions.push_back(version);
 
   if (!handshaker_delegate()->FillTransportParameters(&params)) {
     return false;
@@ -284,6 +288,23 @@
       return false;
     }
   }
+  if (received_transport_params_->version_information.has_value()) {
+    if (!CryptoUtils::ValidateChosenVersion(
+            received_transport_params_->version_information.value()
+                .chosen_version,
+            session()->version(), error_details)) {
+      QUICHE_DCHECK(!error_details->empty());
+      return false;
+    }
+    if (!CryptoUtils::CryptoUtils::ValidateServerVersions(
+            received_transport_params_->version_information.value()
+                .other_versions,
+            session()->version(),
+            session()->client_original_supported_versions(), error_details)) {
+      QUICHE_DCHECK(!error_details->empty());
+      return false;
+    }
+  }
 
   if (handshaker_delegate()->ProcessTransportParameters(
           *received_transport_params_, /* is_resumption = */ false,
diff --git a/quic/core/tls_server_handshaker.cc b/quic/core/tls_server_handshaker.cc
index 6566c8a..9d66887 100644
--- a/quic/core/tls_server_handshaker.cc
+++ b/quic/core/tls_server_handshaker.cc
@@ -508,6 +508,14 @@
     return false;
   }
 
+  if (client_params.version_information.has_value() &&
+      !CryptoUtils::ValidateChosenVersion(
+          client_params.version_information.value().chosen_version,
+          session()->version(), error_details)) {
+    QUICHE_DCHECK(!error_details->empty());
+    return false;
+  }
+
   if (handshaker_delegate()->ProcessTransportParameters(
           client_params, /* is_resumption = */ false, error_details) !=
       QUIC_NO_ERROR) {
@@ -536,6 +544,15 @@
       CreateQuicVersionLabelVector(session()->supported_versions());
   server_params.legacy_version_information.value().version =
       CreateQuicVersionLabel(session()->connection()->version());
+  if (GetQuicReloadableFlag(quic_version_information)) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_version_information, 1, 2);
+    server_params.version_information =
+        TransportParameters::VersionInformation();
+    server_params.version_information.value().chosen_version =
+        CreateQuicVersionLabel(session()->version());
+    server_params.version_information.value().other_versions =
+        CreateQuicVersionLabelVector(session()->supported_versions());
+  }
 
   if (!handshaker_delegate()->FillTransportParameters(&server_params)) {
     return result;
diff --git a/quic/tools/quic_client_base.cc b/quic/tools/quic_client_base.cc
index 6d10de2..e11e25b 100644
--- a/quic/tools/quic_client_base.cc
+++ b/quic/tools/quic_client_base.cc
@@ -185,6 +185,9 @@
                          server_address(), helper(), alarm_factory(), writer,
                          /* owns_writer= */ false, Perspective::IS_CLIENT,
                          client_supported_versions));
+  if (can_reconnect_with_different_version) {
+    session()->set_client_original_supported_versions(supported_versions());
+  }
   if (connection_debug_visitor_ != nullptr) {
     session()->connection()->set_debug_visitor(connection_debug_visitor_);
   }