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 "net/base/features.h"
23 #include "net/base/network_anonymization_key.h"
24 #include "net/base/schemeful_site.h"
25 #include "net/http/transport_security_state.h"
26 #include "net/test/test_with_task_environment.h"
27 #include "testing/gtest/include/gtest/gtest.h"
28 #include "url/gurl.h"
29
30 namespace net {
31
32 namespace {
33
34 const char kReportUri[] = "http://www.example.test/report";
35
36 class TransportSecurityPersisterTest : public ::testing::Test,
37 public WithTaskEnvironment {
38 public:
TransportSecurityPersisterTest()39 TransportSecurityPersisterTest()
40 : WithTaskEnvironment(
41 base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
42 // Mock out time so that entries with hard-coded json data can be
43 // successfully loaded. Use a large enough value that dynamically created
44 // entries have at least somewhat interesting expiration times.
45 FastForwardBy(base::Days(3660));
46 }
47
~TransportSecurityPersisterTest()48 ~TransportSecurityPersisterTest() override {
49 EXPECT_TRUE(base::CurrentIOThread::IsSet());
50 base::RunLoop().RunUntilIdle();
51 }
52
SetUp()53 void SetUp() override {
54 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
55 transport_security_file_path_ =
56 temp_dir_.GetPath().AppendASCII("TransportSecurity");
57 ASSERT_TRUE(base::CurrentIOThread::IsSet());
58 scoped_refptr<base::SequencedTaskRunner> background_runner(
59 base::ThreadPool::CreateSequencedTaskRunner(
60 {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
61 base::TaskShutdownBehavior::BLOCK_SHUTDOWN}));
62 state_ = std::make_unique<TransportSecurityState>();
63 persister_ = std::make_unique<TransportSecurityPersister>(
64 state_.get(), std::move(background_runner),
65 transport_security_file_path_);
66 }
67
68 protected:
69 base::FilePath transport_security_file_path_;
70 base::ScopedTempDir temp_dir_;
71 std::unique_ptr<TransportSecurityState> state_;
72 std::unique_ptr<TransportSecurityPersister> persister_;
73 };
74
75 // Tests that LoadEntries() clears existing non-static entries.
TEST_F(TransportSecurityPersisterTest,LoadEntriesClearsExistingState)76 TEST_F(TransportSecurityPersisterTest, LoadEntriesClearsExistingState) {
77 TransportSecurityState::STSState sts_state;
78 const base::Time current_time(base::Time::Now());
79 const base::Time expiry = current_time + base::Seconds(1000);
80 static const char kYahooDomain[] = "yahoo.com";
81
82 EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
83
84 state_->AddHSTS(kYahooDomain, expiry, false /* include subdomains */);
85 EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
86
87 persister_->LoadEntries("{\"version\":2}");
88
89 EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
90 }
91
92 // Tests that serializing -> deserializing -> reserializing results in the same
93 // output.
TEST_F(TransportSecurityPersisterTest,SerializeData1)94 TEST_F(TransportSecurityPersisterTest, SerializeData1) {
95 std::optional<std::string> output = persister_->SerializeData();
96
97 ASSERT_TRUE(output);
98 persister_->LoadEntries(*output);
99
100 std::optional<std::string> output2 = persister_->SerializeData();
101 ASSERT_TRUE(output2);
102 EXPECT_EQ(output, output2);
103 }
104
TEST_F(TransportSecurityPersisterTest,SerializeData2)105 TEST_F(TransportSecurityPersisterTest, SerializeData2) {
106 TransportSecurityState::STSState sts_state;
107 const base::Time current_time(base::Time::Now());
108 const base::Time expiry = current_time + base::Seconds(1000);
109 static const char kYahooDomain[] = "yahoo.com";
110
111 EXPECT_FALSE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
112
113 bool include_subdomains = true;
114 state_->AddHSTS(kYahooDomain, expiry, include_subdomains);
115
116 std::optional<std::string> output = persister_->SerializeData();
117 ASSERT_TRUE(output);
118 persister_->LoadEntries(*output);
119
120 EXPECT_TRUE(state_->GetDynamicSTSState(kYahooDomain, &sts_state));
121 EXPECT_EQ(sts_state.upgrade_mode,
122 TransportSecurityState::STSState::MODE_FORCE_HTTPS);
123 EXPECT_TRUE(state_->GetDynamicSTSState("foo.yahoo.com", &sts_state));
124 EXPECT_EQ(sts_state.upgrade_mode,
125 TransportSecurityState::STSState::MODE_FORCE_HTTPS);
126 EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.yahoo.com", &sts_state));
127 EXPECT_EQ(sts_state.upgrade_mode,
128 TransportSecurityState::STSState::MODE_FORCE_HTTPS);
129 EXPECT_TRUE(state_->GetDynamicSTSState("foo.bar.baz.yahoo.com", &sts_state));
130 EXPECT_EQ(sts_state.upgrade_mode,
131 TransportSecurityState::STSState::MODE_FORCE_HTTPS);
132 }
133
TEST_F(TransportSecurityPersisterTest,SerializeData3)134 TEST_F(TransportSecurityPersisterTest, SerializeData3) {
135 const GURL report_uri(kReportUri);
136 // Add an entry.
137 base::Time expiry = base::Time::Now() + base::Seconds(1000);
138 bool include_subdomains = false;
139 state_->AddHSTS("www.example.com", expiry, include_subdomains);
140
141 // Add another entry.
142 expiry = base::Time::Now() + base::Seconds(3000);
143 state_->AddHSTS("www.example.net", expiry, include_subdomains);
144
145 // Save a copy of everything.
146 std::set<TransportSecurityState::HashedHost> sts_saved;
147 TransportSecurityState::STSStateIterator sts_iter(*state_);
148 while (sts_iter.HasNext()) {
149 sts_saved.insert(sts_iter.hostname());
150 sts_iter.Advance();
151 }
152
153 std::optional<std::string> serialized = persister_->SerializeData();
154 ASSERT_TRUE(serialized);
155
156 // Persist the data to the file.
157 base::RunLoop run_loop;
158 persister_->WriteNow(state_.get(), run_loop.QuitClosure());
159 run_loop.Run();
160
161 // Read the data back.
162 std::string persisted;
163 EXPECT_TRUE(
164 base::ReadFileToString(transport_security_file_path_, &persisted));
165 EXPECT_EQ(persisted, serialized);
166 persister_->LoadEntries(persisted);
167
168 // Check that states are the same as saved.
169 size_t count = 0;
170 TransportSecurityState::STSStateIterator sts_iter2(*state_);
171 while (sts_iter2.HasNext()) {
172 count++;
173 sts_iter2.Advance();
174 }
175 EXPECT_EQ(count, sts_saved.size());
176 }
177
178 // Tests that deserializing bad data shouldn't result in any STS entries being
179 // added to the transport security state.
TEST_F(TransportSecurityPersisterTest,DeserializeBadData)180 TEST_F(TransportSecurityPersisterTest, DeserializeBadData) {
181 persister_->LoadEntries("");
182 EXPECT_EQ(0u, state_->num_sts_entries());
183
184 persister_->LoadEntries("Foopy");
185 EXPECT_EQ(0u, state_->num_sts_entries());
186
187 persister_->LoadEntries("15");
188 EXPECT_EQ(0u, state_->num_sts_entries());
189
190 persister_->LoadEntries("[15]");
191 EXPECT_EQ(0u, state_->num_sts_entries());
192
193 persister_->LoadEntries("{\"version\":1}");
194 EXPECT_EQ(0u, state_->num_sts_entries());
195 }
196
TEST_F(TransportSecurityPersisterTest,DeserializeDataOldWithoutCreationDate)197 TEST_F(TransportSecurityPersisterTest, DeserializeDataOldWithoutCreationDate) {
198 // This is an old-style piece of transport state JSON, which has no creation
199 // date.
200 const std::string kInput =
201 "{ "
202 "\"G0EywIek2XnIhLrUjaK4TrHBT1+2TcixDVRXwM3/CCo=\": {"
203 "\"expiry\": 1266815027.983453, "
204 "\"include_subdomains\": false, "
205 "\"mode\": \"strict\" "
206 "}"
207 "}";
208 persister_->LoadEntries(kInput);
209 EXPECT_EQ(0u, state_->num_sts_entries());
210 }
211
TEST_F(TransportSecurityPersisterTest,DeserializeDataOldMergedDictionary)212 TEST_F(TransportSecurityPersisterTest, DeserializeDataOldMergedDictionary) {
213 // This is an old-style piece of transport state JSON, which uses a single
214 // unversioned host-keyed dictionary of merged ExpectCT and HSTS data.
215 const std::string kInput =
216 "{"
217 " \"CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=\": {"
218 " \"expect_ct\": {"
219 " \"expect_ct_enforce\": true,"
220 " \"expect_ct_expiry\": 1590512843.283966,"
221 " \"expect_ct_observed\": 1590511843.284064,"
222 " \"expect_ct_report_uri\": \"https://expect_ct.test/report_uri\""
223 " },"
224 " \"expiry\": 0.0,"
225 " \"mode\": \"default\","
226 " \"sts_include_subdomains\": false,"
227 " \"sts_observed\": 0.0"
228 " },"
229 " \"DkgjGShIBmYtgJcJf5lfX3rTr2S6dqyF+O8IAgjuleE=\": {"
230 " \"expiry\": 1590512843.283966,"
231 " \"mode\": \"force-https\","
232 " \"sts_include_subdomains\": false,"
233 " \"sts_observed\": 1590511843.284025"
234 " },"
235 " \"M5lkNV3JBeoPMlKrTOKRYT+mrUsZCS5eoQWsc9/r1MU=\": {"
236 " \"expect_ct\": {"
237 " \"expect_ct_enforce\": true,"
238 " \"expect_ct_expiry\": 1590512843.283966,"
239 " \"expect_ct_observed\": 1590511843.284098,"
240 " \"expect_ct_report_uri\": \"\""
241 " },"
242 " \"expiry\": 1590512843.283966,"
243 " \"mode\": \"force-https\","
244 " \"sts_include_subdomains\": true,"
245 " \"sts_observed\": 1590511843.284091"
246 " }"
247 "}";
248
249 persister_->LoadEntries(kInput);
250 EXPECT_EQ(0u, state_->num_sts_entries());
251 }
252
TEST_F(TransportSecurityPersisterTest,DeserializeLegacyExpectCTData)253 TEST_F(TransportSecurityPersisterTest, DeserializeLegacyExpectCTData) {
254 const std::string kHost = "CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=";
255 const std::string kInput =
256 R"({"version":2, "sts": [{ "host": ")" + kHost +
257 R"(", "mode": "force-https", "sts_include_subdomains": false, )"
258 R"("sts_observed": 0.0, "expiry": 4825336765.0}], "expect_ct": [{"host":)"
259 R"("CxLbri+JPdi5pZ8/a/2rjyzq+IYs07WJJ1yxjB4Lpw0=", "nak": "test", )"
260 R"("expect_ct_observed": 0.0, "expect_ct_expiry": 4825336765.0, )"
261 R"("expect_ct_enforce": true, "expect_ct_report_uri": ""}]})";
262 LOG(ERROR) << kInput;
263 constexpr auto kDefaultFileWriterCommitInterval = base::Seconds(10);
264 persister_->LoadEntries(kInput);
265 FastForwardBy(kDefaultFileWriterCommitInterval + base::Seconds(1));
266 EXPECT_EQ(1u, state_->num_sts_entries());
267 // Now read the data and check that there are no Expect-CT entries.
268 std::string persisted;
269 ASSERT_TRUE(
270 base::ReadFileToString(transport_security_file_path_, &persisted));
271 // Smoke test that the file contains some data as expected...
272 ASSERT_NE(std::string::npos, persisted.find(kHost));
273 // But it shouldn't contain any Expect-CT data.
274 EXPECT_EQ(std::string::npos, persisted.find("expect_ct"));
275 }
276
277 } // namespace
278
279 } // namespace net
280