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