| // Copyright (c) 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "quic/core/tls_chlo_extractor.h" |
| |
| #include <memory> |
| |
| #include "third_party/boringssl/src/include/openssl/ssl.h" |
| #include "quic/core/http/quic_spdy_client_session.h" |
| #include "quic/core/quic_connection.h" |
| #include "quic/core/quic_packet_writer_wrapper.h" |
| #include "quic/core/quic_types.h" |
| #include "quic/core/quic_versions.h" |
| #include "quic/platform/api/quic_test.h" |
| #include "quic/test_tools/crypto_test_utils.h" |
| #include "quic/test_tools/first_flight.h" |
| #include "quic/test_tools/quic_test_utils.h" |
| #include "quic/test_tools/simple_session_cache.h" |
| |
| namespace quic { |
| namespace test { |
| namespace { |
| |
| using testing::_; |
| using testing::AnyNumber; |
| |
| class TlsChloExtractorTest : public QuicTestWithParam<ParsedQuicVersion> { |
| protected: |
| TlsChloExtractorTest() : version_(GetParam()), server_id_(TestServerId()) {} |
| |
| void Initialize() { packets_ = GetFirstFlightOfPackets(version_, config_); } |
| void Initialize(std::unique_ptr<QuicCryptoClientConfig> crypto_config) { |
| packets_ = GetFirstFlightOfPackets(version_, config_, TestConnectionId(), |
| EmptyQuicConnectionId(), |
| std::move(crypto_config)); |
| } |
| |
| // Perform a full handshake in order to insert a SSL_SESSION into |
| // crypto_config->session_cache(), which can be used by a TLS resumption. |
| void PerformFullHandshake(QuicCryptoClientConfig* crypto_config) const { |
| ASSERT_NE(crypto_config->session_cache(), nullptr); |
| MockQuicConnectionHelper client_helper, server_helper; |
| MockAlarmFactory alarm_factory; |
| ParsedQuicVersionVector supported_versions = {version_}; |
| PacketSavingConnection* client_connection = |
| new PacketSavingConnection(&client_helper, &alarm_factory, |
| Perspective::IS_CLIENT, supported_versions); |
| // Advance the time, because timers do not like uninitialized times. |
| client_connection->AdvanceTime(QuicTime::Delta::FromSeconds(1)); |
| QuicClientPushPromiseIndex push_promise_index; |
| QuicSpdyClientSession client_session(config_, supported_versions, |
| client_connection, server_id_, |
| crypto_config, &push_promise_index); |
| client_session.Initialize(); |
| |
| std::unique_ptr<QuicCryptoServerConfig> server_crypto_config = |
| crypto_test_utils::CryptoServerConfigForTesting(); |
| QuicConfig server_config; |
| |
| EXPECT_CALL(*client_connection, SendCryptoData(_, _, _)).Times(AnyNumber()); |
| client_session.GetMutableCryptoStream()->CryptoConnect(); |
| |
| crypto_test_utils::HandshakeWithFakeServer( |
| &server_config, server_crypto_config.get(), &server_helper, |
| &alarm_factory, client_connection, |
| client_session.GetMutableCryptoStream(), |
| AlpnForVersion(client_connection->version())); |
| |
| // For some reason, the test client can not receive the server settings and |
| // the SSL_SESSION will not be inserted to client's session_cache. We create |
| // a dummy settings and call SetServerApplicationStateForResumption manually |
| // to ensure the SSL_SESSION is cached. |
| // TODO(wub): Fix crypto_test_utils::HandshakeWithFakeServer to make sure a |
| // SSL_SESSION is cached at the client, and remove the rest of the function. |
| SettingsFrame server_settings; |
| server_settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = |
| kDefaultQpackMaxDynamicTableCapacity; |
| std::unique_ptr<char[]> buffer; |
| uint64_t length = |
| HttpEncoder::SerializeSettingsFrame(server_settings, &buffer); |
| client_session.GetMutableCryptoStream() |
| ->SetServerApplicationStateForResumption( |
| std::make_unique<ApplicationState>(buffer.get(), |
| buffer.get() + length)); |
| } |
| |
| void IngestPackets() { |
| for (const std::unique_ptr<QuicReceivedPacket>& packet : packets_) { |
| ReceivedPacketInfo packet_info( |
| QuicSocketAddress(TestPeerIPAddress(), kTestPort), |
| QuicSocketAddress(TestPeerIPAddress(), kTestPort), *packet); |
| std::string detailed_error; |
| absl::optional<absl::string_view> retry_token; |
| const QuicErrorCode error = QuicFramer::ParsePublicHeaderDispatcher( |
| *packet, /*expected_destination_connection_id_length=*/0, |
| &packet_info.form, &packet_info.long_packet_type, |
| &packet_info.version_flag, &packet_info.use_length_prefix, |
| &packet_info.version_label, &packet_info.version, |
| &packet_info.destination_connection_id, |
| &packet_info.source_connection_id, &retry_token, &detailed_error); |
| ASSERT_THAT(error, IsQuicNoError()) << detailed_error; |
| tls_chlo_extractor_.IngestPacket(packet_info.version, packet_info.packet); |
| } |
| packets_.clear(); |
| } |
| |
| void ValidateChloDetails() { |
| EXPECT_TRUE(tls_chlo_extractor_.HasParsedFullChlo()); |
| std::vector<std::string> alpns = tls_chlo_extractor_.alpns(); |
| ASSERT_EQ(alpns.size(), 1u); |
| EXPECT_EQ(alpns[0], AlpnForVersion(version_)); |
| EXPECT_EQ(tls_chlo_extractor_.server_name(), TestHostname()); |
| } |
| |
| void IncreaseSizeOfChlo() { |
| // Add a 2000-byte custom parameter to increase the length of the CHLO. |
| constexpr auto kCustomParameterId = |
| static_cast<TransportParameters::TransportParameterId>(0xff33); |
| std::string kCustomParameterValue(2000, '-'); |
| config_.custom_transport_parameters_to_send()[kCustomParameterId] = |
| kCustomParameterValue; |
| } |
| |
| ParsedQuicVersion version_; |
| QuicServerId server_id_; |
| TlsChloExtractor tls_chlo_extractor_; |
| QuicConfig config_; |
| std::vector<std::unique_ptr<QuicReceivedPacket>> packets_; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(TlsChloExtractorTests, |
| TlsChloExtractorTest, |
| ::testing::ValuesIn(AllSupportedVersionsWithTls()), |
| ::testing::PrintToStringParamName()); |
| |
| TEST_P(TlsChloExtractorTest, Simple) { |
| Initialize(); |
| EXPECT_EQ(packets_.size(), 1u); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullSinglePacketChlo); |
| EXPECT_FALSE(tls_chlo_extractor_.resumption_attempted()); |
| EXPECT_FALSE(tls_chlo_extractor_.early_data_attempted()); |
| } |
| |
| TEST_P(TlsChloExtractorTest, TlsExtentionInfo_ResumptionOnly) { |
| auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>( |
| crypto_test_utils::ProofVerifierForTesting(), |
| std::make_unique<SimpleSessionCache>()); |
| PerformFullHandshake(crypto_client_config.get()); |
| |
| SSL_CTX_set_early_data_enabled(crypto_client_config->ssl_ctx(), 0); |
| Initialize(std::move(crypto_client_config)); |
| EXPECT_GE(packets_.size(), 1u); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullSinglePacketChlo); |
| EXPECT_TRUE(tls_chlo_extractor_.resumption_attempted()); |
| EXPECT_FALSE(tls_chlo_extractor_.early_data_attempted()); |
| } |
| |
| TEST_P(TlsChloExtractorTest, TlsExtentionInfo_ZeroRtt) { |
| auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>( |
| crypto_test_utils::ProofVerifierForTesting(), |
| std::make_unique<SimpleSessionCache>()); |
| PerformFullHandshake(crypto_client_config.get()); |
| |
| IncreaseSizeOfChlo(); |
| Initialize(std::move(crypto_client_config)); |
| EXPECT_GE(packets_.size(), 1u); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullMultiPacketChlo); |
| EXPECT_TRUE(tls_chlo_extractor_.resumption_attempted()); |
| EXPECT_TRUE(tls_chlo_extractor_.early_data_attempted()); |
| } |
| |
| TEST_P(TlsChloExtractorTest, MultiPacket) { |
| IncreaseSizeOfChlo(); |
| Initialize(); |
| EXPECT_EQ(packets_.size(), 2u); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullMultiPacketChlo); |
| } |
| |
| TEST_P(TlsChloExtractorTest, MultiPacketReordered) { |
| IncreaseSizeOfChlo(); |
| Initialize(); |
| ASSERT_EQ(packets_.size(), 2u); |
| // Artifically reorder both packets. |
| std::swap(packets_[0], packets_[1]); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullMultiPacketChlo); |
| } |
| |
| TEST_P(TlsChloExtractorTest, MoveAssignment) { |
| Initialize(); |
| EXPECT_EQ(packets_.size(), 1u); |
| TlsChloExtractor other_extractor; |
| tls_chlo_extractor_ = std::move(other_extractor); |
| IngestPackets(); |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullSinglePacketChlo); |
| } |
| |
| TEST_P(TlsChloExtractorTest, MoveAssignmentBetweenPackets) { |
| IncreaseSizeOfChlo(); |
| Initialize(); |
| ASSERT_EQ(packets_.size(), 2u); |
| TlsChloExtractor other_extractor; |
| |
| // Have |other_extractor| parse the first packet. |
| ReceivedPacketInfo packet_info( |
| QuicSocketAddress(TestPeerIPAddress(), kTestPort), |
| QuicSocketAddress(TestPeerIPAddress(), kTestPort), *packets_[0]); |
| std::string detailed_error; |
| absl::optional<absl::string_view> retry_token; |
| const QuicErrorCode error = QuicFramer::ParsePublicHeaderDispatcher( |
| *packets_[0], /*expected_destination_connection_id_length=*/0, |
| &packet_info.form, &packet_info.long_packet_type, |
| &packet_info.version_flag, &packet_info.use_length_prefix, |
| &packet_info.version_label, &packet_info.version, |
| &packet_info.destination_connection_id, &packet_info.source_connection_id, |
| &retry_token, &detailed_error); |
| ASSERT_THAT(error, IsQuicNoError()) << detailed_error; |
| other_extractor.IngestPacket(packet_info.version, packet_info.packet); |
| // Remove the first packet from the list. |
| packets_.erase(packets_.begin()); |
| EXPECT_EQ(packets_.size(), 1u); |
| |
| // Move the extractor. |
| tls_chlo_extractor_ = std::move(other_extractor); |
| |
| // Have |tls_chlo_extractor_| parse the second packet. |
| IngestPackets(); |
| |
| ValidateChloDetails(); |
| EXPECT_EQ(tls_chlo_extractor_.state(), |
| TlsChloExtractor::State::kParsedFullMultiPacketChlo); |
| } |
| |
| } // namespace |
| } // namespace test |
| } // namespace quic |