• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "net/http/transport_security_persister.h"
6 
7 #include <map>
8 #include <memory>
9 #include <string>
10 #include <vector>
11 
12 #include "base/files/file_path.h"
13 #include "base/files/file_util.h"
14 #include "base/files/scoped_temp_dir.h"
15 #include "base/json/json_writer.h"
16 #include "base/run_loop.h"
17 #include "base/strings/string_util.h"
18 #include "base/task/current_thread.h"
19 #include "base/task/sequenced_task_runner.h"
20 #include "base/task/thread_pool.h"
21 #include "base/test/scoped_feature_list.h"
22 #include "base/time/time.h"
23 #include "net/base/features.h"
24 #include "net/base/network_anonymization_key.h"
25 #include "net/base/schemeful_site.h"
26 #include "net/http/transport_security_state.h"
27 #include "net/test/test_with_task_environment.h"
28 #include "testing/gtest/include/gtest/gtest.h"
29 #include "url/gurl.h"
30 
31 namespace net {
32 
33 namespace {
34 
35 const char kReportUri[] = "http://www.example.test/report";
36 
37 class TransportSecurityPersisterTest : public ::testing::Test,
38                                        public WithTaskEnvironment {
39  public:
TransportSecurityPersisterTest()40   TransportSecurityPersisterTest()
41       : WithTaskEnvironment(
42             base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
43     // Mock out time so that entries with hard-coded json data can be
44     // successfully loaded. Use a large enough value that dynamically created
45     // entries have at least somewhat interesting expiration times.
46     FastForwardBy(base::Days(3660));
47   }
48 
~TransportSecurityPersisterTest()49   ~TransportSecurityPersisterTest() override {
50     EXPECT_TRUE(base::CurrentIOThread::IsSet());
51     base::RunLoop().RunUntilIdle();
52   }
53 
SetUp()54   void SetUp() override {
55     ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
56     transport_security_file_path_ =
57         temp_dir_.GetPath().AppendASCII("TransportSecurity");
58     ASSERT_TRUE(base::CurrentIOThread::IsSet());
59     scoped_refptr<base::SequencedTaskRunner> background_runner(
60         base::ThreadPool::CreateSequencedTaskRunner(
61             {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
62              base::TaskShutdownBehavior::BLOCK_SHUTDOWN}));
63     state_ = std::make_unique<TransportSecurityState>();
64     persister_ = std::make_unique<TransportSecurityPersister>(
65         state_.get(), std::move(background_runner),
66         transport_security_file_path_);
67   }
68 
69  protected:
70   base::FilePath transport_security_file_path_;
71   base::ScopedTempDir temp_dir_;
72   std::unique_ptr<TransportSecurityState> state_;
73   std::unique_ptr<TransportSecurityPersister> persister_;
74 };
75 
76 // Tests that LoadEntries() clears existing non-static entries.
TEST_F(TransportSecurityPersisterTest,LoadEntriesClearsExistingState)77 TEST_F(TransportSecurityPersisterTest, LoadEntriesClearsExistingState) {
78   TransportSecurityState::STSState sts_state;
79   const base::Time current_time(base::Time::Now());
80   const base::Time expiry = current_time + base::Seconds(1000);
81   static const char kYahooDomain[] = "yahoo.com";
82 
83   EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
84 
85   state_->AddHSTS(kYahooDomain, expiry, false /* include subdomains */);
86   EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
87 
88   persister_->LoadEntries("{\"version\":2}");
89 
90   EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
91 }
92 
93 // Tests that serializing -> deserializing -> reserializing results in the same
94 // output.
TEST_F(TransportSecurityPersisterTest,SerializeData1)95 TEST_F(TransportSecurityPersisterTest, SerializeData1) {
96   std::optional<std::string> output = persister_->SerializeData();
97 
98   ASSERT_TRUE(output);
99   persister_->LoadEntries(*output);
100 
101   std::optional<std::string> output2 = persister_->SerializeData();
102   ASSERT_TRUE(output2);
103   EXPECT_EQ(output, output2);
104 }
105 
TEST_F(TransportSecurityPersisterTest,SerializeData2)106 TEST_F(TransportSecurityPersisterTest, SerializeData2) {
107   TransportSecurityState::STSState sts_state;
108   const base::Time current_time(base::Time::Now());
109   const base::Time expiry = current_time + base::Seconds(1000);
110   static const char kYahooDomain[] = "yahoo.com";
111 
112   EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
113 
114   bool include_subdomains = true;
115   state_->AddHSTS(kYahooDomain, expiry, include_subdomains);
116 
117   std::optional<std::string> output = persister_->SerializeData();
118   ASSERT_TRUE(output);
119   persister_->LoadEntries(*output);
120 
121   EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
122   EXPECT_EQ(sts_state.upgrade_mode,
123             TransportSecurityState::STSState::MODE_FORCE_HTTPS);
124   EXPECT_TRUE(state_->GetDynamicSTSState("foo.yahoo.com", &sts_state));
125   EXPECT_EQ(sts_state.upgrade_mode,
126             TransportSecurityState::STSState::MODE_FORCE_HTTPS);
127   EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.yahoo.com", &sts_state));
128   EXPECT_EQ(sts_state.upgrade_mode,
129             TransportSecurityState::STSState::MODE_FORCE_HTTPS);
130   EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.baz.yahoo.com", &sts_state));
131   EXPECT_EQ(sts_state.upgrade_mode,
132             TransportSecurityState::STSState::MODE_FORCE_HTTPS);
133 }
134 
TEST_F(TransportSecurityPersisterTest,SerializeData3)135 TEST_F(TransportSecurityPersisterTest, SerializeData3) {
136   const GURL report_uri(kReportUri);
137   // Add an entry.
138   base::Time expiry = base::Time::Now() + base::Seconds(1000);
139   bool include_subdomains = false;
140   state_->AddHSTS("www.example.com", expiry, include_subdomains);
141 
142   // Add another entry.
143   expiry = base::Time::Now() + base::Seconds(3000);
144   state_->AddHSTS("www.example.net", expiry, include_subdomains);
145 
146   // Save a copy of everything.
147   std::set<TransportSecurityState::HashedHost> sts_saved;
148   TransportSecurityState::STSStateIterator sts_iter(*state_);
149   while (sts_iter.HasNext()) {
150     sts_saved.insert(sts_iter.hostname());
151     sts_iter.Advance();
152   }
153 
154   std::optional<std::string> serialized = persister_->SerializeData();
155   ASSERT_TRUE(serialized);
156 
157   // Persist the data to the file.
158   base::RunLoop run_loop;
159   persister_->WriteNow(state_.get(), run_loop.QuitClosure());
160   run_loop.Run();
161 
162   // Read the data back.
163   std::string persisted;
164   EXPECT_TRUE(
165       base::ReadFileToString(transport_security_file_path_, &persisted));
166   EXPECT_EQ(persisted, serialized);
167   persister_->LoadEntries(persisted);
168 
169   // Check that states are the same as saved.
170   size_t count = 0;
171   TransportSecurityState::STSStateIterator sts_iter2(*state_);
172   while (sts_iter2.HasNext()) {
173     count++;
174     sts_iter2.Advance();
175   }
176   EXPECT_EQ(count, sts_saved.size());
177 }
178 
179 // Tests that deserializing bad data shouldn't result in any STS entries being
180 // added to the transport security state.
TEST_F(TransportSecurityPersisterTest,DeserializeBadData)181 TEST_F(TransportSecurityPersisterTest, DeserializeBadData) {
182   persister_->LoadEntries("");
183   EXPECT_EQ(0u, state_->num_sts_entries());
184 
185   persister_->LoadEntries("Foopy");
186   EXPECT_EQ(0u, state_->num_sts_entries());
187 
188   persister_->LoadEntries("15");
189   EXPECT_EQ(0u, state_->num_sts_entries());
190 
191   persister_->LoadEntries("[15]");
192   EXPECT_EQ(0u, state_->num_sts_entries());
193 
194   persister_->LoadEntries("{\"version\":1}");
195   EXPECT_EQ(0u, state_->num_sts_entries());
196 }
197 
TEST_F(TransportSecurityPersisterTest,DeserializeDataOldWithoutCreationDate)198 TEST_F(TransportSecurityPersisterTest, DeserializeDataOldWithoutCreationDate) {
199   // This is an old-style piece of transport state JSON, which has no creation
200   // date.
201   const std::string kInput =
202       "{ "
203       "\"G0EywIek2XnIhLrUjaK4TrHBT1+2TcixDVRXwM3/CCo=\": {"
204       "\"expiry\": 1266815027.983453, "
205       "\"include_subdomains\": false, "
206       "\"mode\": \"strict\" "
207       "}"
208       "}";
209   persister_->LoadEntries(kInput);
210   EXPECT_EQ(0u, state_->num_sts_entries());
211 }
212 
TEST_F(TransportSecurityPersisterTest,DeserializeDataOldMergedDictionary)213 TEST_F(TransportSecurityPersisterTest, DeserializeDataOldMergedDictionary) {
214   // This is an old-style piece of transport state JSON, which uses a single
215   // unversioned host-keyed dictionary of merged ExpectCT and HSTS data.
216   const std::string kInput =
217       "{"
218       "   \"CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=\": {"
219       "      \"expect_ct\": {"
220       "         \"expect_ct_enforce\": true,"
221       "         \"expect_ct_expiry\": 1590512843.283966,"
222       "         \"expect_ct_observed\": 1590511843.284064,"
223       "         \"expect_ct_report_uri\": \"https://expect_ct.test/report_uri\""
224       "      },"
225       "      \"expiry\": 0.0,"
226       "      \"mode\": \"default\","
227       "      \"sts_include_subdomains\": false,"
228       "      \"sts_observed\": 0.0"
229       "   },"
230       "   \"DkgjGShIBmYtgJcJf5lfX3rTr2S6dqyF+O8IAgjuleE=\": {"
231       "      \"expiry\": 1590512843.283966,"
232       "      \"mode\": \"force-https\","
233       "      \"sts_include_subdomains\": false,"
234       "      \"sts_observed\": 1590511843.284025"
235       "   },"
236       "   \"M5lkNV3JBeoPMlKrTOKRYT+mrUsZCS5eoQWsc9/r1MU=\": {"
237       "      \"expect_ct\": {"
238       "         \"expect_ct_enforce\": true,"
239       "         \"expect_ct_expiry\": 1590512843.283966,"
240       "         \"expect_ct_observed\": 1590511843.284098,"
241       "         \"expect_ct_report_uri\": \"\""
242       "      },"
243       "      \"expiry\": 1590512843.283966,"
244       "      \"mode\": \"force-https\","
245       "      \"sts_include_subdomains\": true,"
246       "      \"sts_observed\": 1590511843.284091"
247       "   }"
248       "}";
249 
250   persister_->LoadEntries(kInput);
251   EXPECT_EQ(0u, state_->num_sts_entries());
252 }
253 
TEST_F(TransportSecurityPersisterTest,DeserializeLegacyExpectCTData)254 TEST_F(TransportSecurityPersisterTest, DeserializeLegacyExpectCTData) {
255   const std::string kHost = "CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=";
256   const std::string kInput =
257       R"({"version":2, "sts": [{ "host": ")" + kHost +
258       R"(", "mode": "force-https", "sts_include_subdomains": false, )"
259       R"("sts_observed": 0.0, "expiry": 4825336765.0}], "expect_ct": [{"host":)"
260       R"("CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=", "nak": "test", )"
261       R"("expect_ct_observed": 0.0, "expect_ct_expiry": 4825336765.0, )"
262       R"("expect_ct_enforce": true, "expect_ct_report_uri": ""}]})";
263   LOG(ERROR) << kInput;
264   persister_->LoadEntries(kInput);
265   FastForwardBy(TransportSecurityPersister::GetCommitInterval() +
266                 base::Seconds(1));
267   EXPECT_EQ(1u, state_->num_sts_entries());
268   // Now read the data and check that there are no Expect-CT entries.
269   std::string persisted;
270   ASSERT_TRUE(
271       base::ReadFileToString(transport_security_file_path_, &persisted));
272   // Smoke test that the file contains some data as expected...
273   ASSERT_NE(std::string::npos, persisted.find(kHost));
274   // But it shouldn't contain any Expect-CT data.
275   EXPECT_EQ(std::string::npos, persisted.find("expect_ct"));
276 }
277 
278 class TransportSecurityPersisterCommitTest
279     : public TransportSecurityPersisterTest,
280       public ::testing::WithParamInterface<std::string> {
281  public:
TransportSecurityPersisterCommitTest()282   TransportSecurityPersisterCommitTest() {
283     if (GetParam().empty()) {
284       feature_list_.InitAndDisableFeature(kTransportSecurityFileWriterSchedule);
285     } else {
286       feature_list_.InitAndEnableFeatureWithParameters(
287           kTransportSecurityFileWriterSchedule,
288           {{"commit_interval", GetParam()}});
289     }
290   }
291 
292  private:
293   base::test::ScopedFeatureList feature_list_;
294 };
295 
296 INSTANTIATE_TEST_SUITE_P(
297     All,
298     TransportSecurityPersisterCommitTest,
299     ::testing::Values(
300         // The ImportantFileWriter default.
301         "10s",
302         // Anything less should use the default.
303         "9s",
304         "0",
305         "-10s",
306         "-inf",
307         // Valid values.
308         "1m",
309         "10m",
310         // Anything greater should use the max.
311         "11m",
312         "+inf",
313         // Disable the feature. Should use the default interval.
314         ""));
315 
TEST_P(TransportSecurityPersisterCommitTest,CommitIntervalIsValid)316 TEST_P(TransportSecurityPersisterCommitTest, CommitIntervalIsValid) {
317   EXPECT_GE(TransportSecurityPersister::GetCommitInterval(), base::Seconds(10));
318   EXPECT_LE(TransportSecurityPersister::GetCommitInterval(), base::Minutes(10));
319 }
320 
TEST_P(TransportSecurityPersisterCommitTest,WriteAtCommitInterval)321 TEST_P(TransportSecurityPersisterCommitTest, WriteAtCommitInterval) {
322   const auto kLongExpiry = base::Time::Now() + base::Days(10);
323   const bool kIncludeSubdomains = false;
324 
325   // Make sure the file starts empty.
326   ASSERT_TRUE(base::WriteFile(transport_security_file_path_, ""));
327 
328   // Add an entry. Expect the persister NOT to write before the commit interval,
329   // for performance.
330   state_->AddHSTS("www.example.com", kLongExpiry, kIncludeSubdomains);
331   FastForwardBy(TransportSecurityPersister::GetCommitInterval() / 2);
332   std::string persisted;
333   EXPECT_TRUE(
334       base::ReadFileToString(transport_security_file_path_, &persisted));
335   EXPECT_TRUE(persisted.empty());
336 
337   // Add another entry. After the commit interval passes, both should be
338   // written.
339   state_->AddHSTS("www.example.net", kLongExpiry, kIncludeSubdomains);
340   FastForwardBy(TransportSecurityPersister::GetCommitInterval() / 2);
341   EXPECT_TRUE(
342       base::ReadFileToString(transport_security_file_path_, &persisted));
343   EXPECT_FALSE(persisted.empty());
344 
345   // Ensure that state comes from the persisted file.
346   persister_->LoadEntries("");
347   TransportSecurityState::STSState dummy_state;
348   ASSERT_FALSE(state_->GetDynamicSTSState("www.example.com", &dummy_state));
349   ASSERT_FALSE(state_->GetDynamicSTSState("www.example.net", &dummy_state));
350 
351   // Check that both entries were persisted.
352   persister_->LoadEntries(persisted);
353   EXPECT_TRUE(state_->GetDynamicSTSState("www.example.com", &dummy_state));
354   EXPECT_TRUE(state_->GetDynamicSTSState("www.example.net", &dummy_state));
355 
356   // Add a third entry and force a write before the commit interval
357   state_->AddHSTS("www.example.org", kLongExpiry, kIncludeSubdomains);
358 
359   const auto time_before_write = base::TimeTicks::Now();
360   base::RunLoop run_loop;
361   persister_->WriteNow(state_.get(), run_loop.QuitClosure());
362   run_loop.Run();
363   EXPECT_LT(base::TimeTicks::Now() - time_before_write,
364             TransportSecurityPersister::GetCommitInterval());
365   EXPECT_TRUE(
366       base::ReadFileToString(transport_security_file_path_, &persisted));
367   EXPECT_FALSE(persisted.empty());
368 
369   // Ensure that state comes from the persisted file.
370   persister_->LoadEntries("");
371   ASSERT_FALSE(state_->GetDynamicSTSState("www.example.com", &dummy_state));
372   ASSERT_FALSE(state_->GetDynamicSTSState("www.example.net", &dummy_state));
373   ASSERT_FALSE(state_->GetDynamicSTSState("www.example.org", &dummy_state));
374 
375   // Check that all entries were persisted.
376   persister_->LoadEntries(persisted);
377   EXPECT_TRUE(state_->GetDynamicSTSState("www.example.com", &dummy_state));
378   EXPECT_TRUE(state_->GetDynamicSTSState("www.example.net", &dummy_state));
379   EXPECT_TRUE(state_->GetDynamicSTSState("www.example.org", &dummy_state));
380 }
381 
382 }  // namespace
383 
384 }  // namespace net
385