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(¶ms)) { 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_); }