| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/http/transport_security_persister.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/json/json_writer.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_util.h" |
| #include "base/task/current_thread.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "net/base/features.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/http/transport_security_state.h" |
| #include "net/test/test_with_task_environment.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| const char kReportUri[] = "http://www.example.test/report"; |
| |
| class TransportSecurityPersisterTest : public ::testing::Test, |
| public WithTaskEnvironment { |
| public: |
| TransportSecurityPersisterTest() |
| : WithTaskEnvironment( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| // Mock out time so that entries with hard-coded json data can be |
| // successfully loaded. Use a large enough value that dynamically created |
| // entries have at least somewhat interesting expiration times. |
| FastForwardBy(base::Days(3660)); |
| } |
| |
| ~TransportSecurityPersisterTest() override { |
| EXPECT_TRUE(base::CurrentIOThread::IsSet()); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| transport_security_file_path_ = |
| temp_dir_.GetPath().AppendASCII("TransportSecurity"); |
| ASSERT_TRUE(base::CurrentIOThread::IsSet()); |
| scoped_refptr<base::SequencedTaskRunner> background_runner( |
| base::ThreadPool::CreateSequencedTaskRunner( |
| {base::MayBlock(), base::TaskPriority::BEST_EFFORT, |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN})); |
| state_ = std::make_unique<TransportSecurityState>(); |
| persister_ = std::make_unique<TransportSecurityPersister>( |
| state_.get(), std::move(background_runner), |
| transport_security_file_path_); |
| } |
| |
| protected: |
| base::FilePath transport_security_file_path_; |
| base::ScopedTempDir temp_dir_; |
| std::unique_ptr<TransportSecurityState> state_; |
| std::unique_ptr<TransportSecurityPersister> persister_; |
| }; |
| |
| // Tests that LoadEntries() clears existing non-static entries. |
| TEST_F(TransportSecurityPersisterTest, LoadEntriesClearsExistingState) { |
| TransportSecurityState::STSState sts_state; |
| const base::Time current_time(base::Time::Now()); |
| const base::Time expiry = current_time + base::Seconds(1000); |
| static const char kYahooDomain[] = "yahoo.com"; |
| |
| EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state)); |
| |
| state_->AddHSTS(kYahooDomain, expiry, false /* include subdomains */); |
| EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state)); |
| |
| persister_->LoadEntries("{\"version\":2}"); |
| |
| EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state)); |
| } |
| |
| // Tests that serializing -> deserializing -> reserializing results in the same |
| // output. |
| TEST_F(TransportSecurityPersisterTest, SerializeData1) { |
| absl::optional<std::string> output = persister_->SerializeData(); |
| |
| ASSERT_TRUE(output); |
| persister_->LoadEntries(*output); |
| |
| absl::optional<std::string> output2 = persister_->SerializeData(); |
| ASSERT_TRUE(output2); |
| EXPECT_EQ(output, output2); |
| } |
| |
| TEST_F(TransportSecurityPersisterTest, SerializeData2) { |
| TransportSecurityState::STSState sts_state; |
| const base::Time current_time(base::Time::Now()); |
| const base::Time expiry = current_time + base::Seconds(1000); |
| static const char kYahooDomain[] = "yahoo.com"; |
| |
| EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state)); |
| |
| bool include_subdomains = true; |
| state_->AddHSTS(kYahooDomain, expiry, include_subdomains); |
| |
| absl::optional<std::string> output = persister_->SerializeData(); |
| ASSERT_TRUE(output); |
| persister_->LoadEntries(*output); |
| |
| EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state)); |
| EXPECT_EQ(sts_state.upgrade_mode, |
| TransportSecurityState::STSState::MODE_FORCE_HTTPS); |
| EXPECT_TRUE(state_->GetDynamicSTSState("foo.yahoo.com", &sts_state)); |
| EXPECT_EQ(sts_state.upgrade_mode, |
| TransportSecurityState::STSState::MODE_FORCE_HTTPS); |
| EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.yahoo.com", &sts_state)); |
| EXPECT_EQ(sts_state.upgrade_mode, |
| TransportSecurityState::STSState::MODE_FORCE_HTTPS); |
| EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.baz.yahoo.com", &sts_state)); |
| EXPECT_EQ(sts_state.upgrade_mode, |
| TransportSecurityState::STSState::MODE_FORCE_HTTPS); |
| } |
| |
| TEST_F(TransportSecurityPersisterTest, SerializeData3) { |
| const GURL report_uri(kReportUri); |
| // Add an entry. |
| base::Time expiry = base::Time::Now() + base::Seconds(1000); |
| bool include_subdomains = false; |
| state_->AddHSTS("www.example.com", expiry, include_subdomains); |
| |
| // Add another entry. |
| expiry = base::Time::Now() + base::Seconds(3000); |
| state_->AddHSTS("www.example.net", expiry, include_subdomains); |
| |
| // Save a copy of everything. |
| std::set<TransportSecurityState::HashedHost> sts_saved; |
| TransportSecurityState::STSStateIterator sts_iter(*state_); |
| while (sts_iter.HasNext()) { |
| sts_saved.insert(sts_iter.hostname()); |
| sts_iter.Advance(); |
| } |
| |
| absl::optional<std::string> serialized = persister_->SerializeData(); |
| ASSERT_TRUE(serialized); |
| |
| // Persist the data to the file. |
| base::RunLoop run_loop; |
| persister_->WriteNow(state_.get(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // Read the data back. |
| std::string persisted; |
| EXPECT_TRUE( |
| base::ReadFileToString(transport_security_file_path_, &persisted)); |
| EXPECT_EQ(persisted, serialized); |
| persister_->LoadEntries(persisted); |
| |
| // Check that states are the same as saved. |
| size_t count = 0; |
| TransportSecurityState::STSStateIterator sts_iter2(*state_); |
| while (sts_iter2.HasNext()) { |
| count++; |
| sts_iter2.Advance(); |
| } |
| EXPECT_EQ(count, sts_saved.size()); |
| } |
| |
| // Tests that deserializing bad data shouldn't result in any STS entries being |
| // added to the transport security state. |
| TEST_F(TransportSecurityPersisterTest, DeserializeBadData) { |
| persister_->LoadEntries(""); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| |
| persister_->LoadEntries("Foopy"); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| |
| persister_->LoadEntries("15"); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| |
| persister_->LoadEntries("[15]"); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| |
| persister_->LoadEntries("{\"version\":1}"); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| } |
| |
| TEST_F(TransportSecurityPersisterTest, DeserializeDataOldWithoutCreationDate) { |
| // This is an old-style piece of transport state JSON, which has no creation |
| // date. |
| const std::string kInput = |
| "{ " |
| "\"G0EywIek2XnIhLrUjaK4TrHBT1+2TcixDVRXwM3/CCo=\": {" |
| "\"expiry\": 1266815027.983453, " |
| "\"include_subdomains\": false, " |
| "\"mode\": \"strict\" " |
| "}" |
| "}"; |
| persister_->LoadEntries(kInput); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| } |
| |
| TEST_F(TransportSecurityPersisterTest, DeserializeDataOldMergedDictionary) { |
| // This is an old-style piece of transport state JSON, which uses a single |
| // unversioned host-keyed dictionary of merged ExpectCT and HSTS data. |
| const std::string kInput = |
| "{" |
| " \"CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=\": {" |
| " \"expect_ct\": {" |
| " \"expect_ct_enforce\": true," |
| " \"expect_ct_expiry\": 1590512843.283966," |
| " \"expect_ct_observed\": 1590511843.284064," |
| " \"expect_ct_report_uri\": \"https://expect_ct.test/report_uri\"" |
| " }," |
| " \"expiry\": 0.0," |
| " \"mode\": \"default\"," |
| " \"sts_include_subdomains\": false," |
| " \"sts_observed\": 0.0" |
| " }," |
| " \"DkgjGShIBmYtgJcJf5lfX3rTr2S6dqyF+O8IAgjuleE=\": {" |
| " \"expiry\": 1590512843.283966," |
| " \"mode\": \"force-https\"," |
| " \"sts_include_subdomains\": false," |
| " \"sts_observed\": 1590511843.284025" |
| " }," |
| " \"M5lkNV3JBeoPMlKrTOKRYT+mrUsZCS5eoQWsc9/r1MU=\": {" |
| " \"expect_ct\": {" |
| " \"expect_ct_enforce\": true," |
| " \"expect_ct_expiry\": 1590512843.283966," |
| " \"expect_ct_observed\": 1590511843.284098," |
| " \"expect_ct_report_uri\": \"\"" |
| " }," |
| " \"expiry\": 1590512843.283966," |
| " \"mode\": \"force-https\"," |
| " \"sts_include_subdomains\": true," |
| " \"sts_observed\": 1590511843.284091" |
| " }" |
| "}"; |
| |
| persister_->LoadEntries(kInput); |
| EXPECT_EQ(0u, state_->num_sts_entries()); |
| } |
| |
| TEST_F(TransportSecurityPersisterTest, DeserializeLegacyExpectCTData) { |
| const std::string kHost = "CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0="; |
| const std::string kInput = |
| R"({"version":2, "sts": [{ "host": ")" + kHost + |
| R"(", "mode": "force-https", "sts_include_subdomains": false, )" |
| R"("sts_observed": 0.0, "expiry": 4825336765.0}], "expect_ct": [{"host":)" |
| R"("CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=", "nak": "test", )" |
| R"("expect_ct_observed": 0.0, "expect_ct_expiry": 4825336765.0, )" |
| R"("expect_ct_enforce": true, "expect_ct_report_uri": ""}]})"; |
| LOG(ERROR) << kInput; |
| constexpr auto kDefaultFileWriterCommitInterval = base::Seconds(10); |
| persister_->LoadEntries(kInput); |
| FastForwardBy(kDefaultFileWriterCommitInterval + base::Seconds(1)); |
| EXPECT_EQ(1u, state_->num_sts_entries()); |
| // Now read the data and check that there are no Expect-CT entries. |
| std::string persisted; |
| ASSERT_TRUE( |
| base::ReadFileToString(transport_security_file_path_, &persisted)); |
| // Smoke test that the file contains some data as expected... |
| ASSERT_NE(std::string::npos, persisted.find(kHost)); |
| // But it shouldn't contain any Expect-CT data. |
| EXPECT_EQ(std::string::npos, persisted.find("expect_ct")); |
| } |
| |
| } // namespace |
| |
| } // namespace net |