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_);
}