OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
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 "chrome/browser/net/transport_security_persister.h" | |
6 | |
7 #include "base/base64.h" | |
8 #include "base/bind.h" | |
9 #include "base/file_util.h" | |
10 #include "base/files/file_path.h" | |
11 #include "base/json/json_reader.h" | |
12 #include "base/json/json_writer.h" | |
13 #include "base/message_loop/message_loop.h" | |
14 #include "base/message_loop/message_loop_proxy.h" | |
15 #include "base/sequenced_task_runner.h" | |
16 #include "base/task_runner_util.h" | |
17 #include "base/values.h" | |
18 #include "crypto/sha2.h" | |
19 #include "net/cert/x509_certificate.h" | |
20 #include "net/http/transport_security_state.h" | |
21 | |
22 using net::HashValue; | |
23 using net::HashValueTag; | |
24 using net::HashValueVector; | |
25 using net::TransportSecurityState; | |
26 | |
27 namespace { | |
28 | |
29 ListValue* SPKIHashesToListValue(const HashValueVector& hashes) { | |
30 ListValue* pins = new ListValue; | |
31 for (size_t i = 0; i != hashes.size(); i++) | |
32 pins->Append(new StringValue(hashes[i].ToString())); | |
33 return pins; | |
34 } | |
35 | |
36 void SPKIHashesFromListValue(const ListValue& pins, HashValueVector* hashes) { | |
37 size_t num_pins = pins.GetSize(); | |
38 for (size_t i = 0; i < num_pins; ++i) { | |
39 std::string type_and_base64; | |
40 HashValue fingerprint; | |
41 if (pins.GetString(i, &type_and_base64) && | |
42 fingerprint.FromString(type_and_base64)) { | |
43 hashes->push_back(fingerprint); | |
44 } | |
45 } | |
46 } | |
47 | |
48 // This function converts the binary hashes to a base64 string which we can | |
49 // include in a JSON file. | |
50 std::string HashedDomainToExternalString(const std::string& hashed) { | |
51 std::string out; | |
52 base::Base64Encode(hashed, &out); | |
53 return out; | |
54 } | |
55 | |
56 // This inverts |HashedDomainToExternalString|, above. It turns an external | |
57 // string (from a JSON file) into an internal (binary) string. | |
58 std::string ExternalStringToHashedDomain(const std::string& external) { | |
59 std::string out; | |
60 if (!base::Base64Decode(external, &out) || | |
61 out.size() != crypto::kSHA256Length) { | |
62 return std::string(); | |
63 } | |
64 | |
65 return out; | |
66 } | |
67 | |
68 const char kIncludeSubdomains[] = "include_subdomains"; | |
69 const char kStsIncludeSubdomains[] = "sts_include_subdomains"; | |
70 const char kPkpIncludeSubdomains[] = "pkp_include_subdomains"; | |
71 const char kMode[] = "mode"; | |
72 const char kExpiry[] = "expiry"; | |
73 const char kDynamicSPKIHashesExpiry[] = "dynamic_spki_hashes_expiry"; | |
74 const char kStaticSPKIHashes[] = "static_spki_hashes"; | |
75 const char kPreloadedSPKIHashes[] = "preloaded_spki_hashes"; | |
76 const char kDynamicSPKIHashes[] = "dynamic_spki_hashes"; | |
77 const char kForceHTTPS[] = "force-https"; | |
78 const char kStrict[] = "strict"; | |
79 const char kDefault[] = "default"; | |
80 const char kPinningOnly[] = "pinning-only"; | |
81 const char kCreated[] = "created"; | |
82 | |
83 std::string LoadState(const base::FilePath& path) { | |
84 std::string result; | |
85 if (!base::ReadFileToString(path, &result)) { | |
86 return ""; | |
87 } | |
88 return result; | |
89 } | |
90 | |
91 } // namespace | |
92 | |
93 TransportSecurityPersister::TransportSecurityPersister( | |
94 TransportSecurityState* state, | |
95 const base::FilePath& profile_path, | |
96 base::SequencedTaskRunner* background_runner, | |
97 bool readonly) | |
98 : transport_security_state_(state), | |
99 writer_(profile_path.AppendASCII("TransportSecurity"), background_runner), | |
100 foreground_runner_(base::MessageLoop::current()->message_loop_proxy()), | |
101 background_runner_(background_runner), | |
102 readonly_(readonly), | |
103 weak_ptr_factory_(this) { | |
104 transport_security_state_->SetDelegate(this); | |
105 | |
106 base::PostTaskAndReplyWithResult( | |
107 background_runner_, | |
108 FROM_HERE, | |
109 base::Bind(&::LoadState, writer_.path()), | |
110 base::Bind(&TransportSecurityPersister::CompleteLoad, | |
111 weak_ptr_factory_.GetWeakPtr())); | |
112 } | |
113 | |
114 TransportSecurityPersister::~TransportSecurityPersister() { | |
115 DCHECK(foreground_runner_->RunsTasksOnCurrentThread()); | |
116 | |
117 if (writer_.HasPendingWrite()) | |
118 writer_.DoScheduledWrite(); | |
119 | |
120 transport_security_state_->SetDelegate(NULL); | |
121 } | |
122 | |
123 void TransportSecurityPersister::StateIsDirty( | |
124 TransportSecurityState* state) { | |
125 DCHECK(foreground_runner_->RunsTasksOnCurrentThread()); | |
126 DCHECK_EQ(transport_security_state_, state); | |
127 | |
128 if (!readonly_) | |
129 writer_.ScheduleWrite(this); | |
130 } | |
131 | |
132 bool TransportSecurityPersister::SerializeData(std::string* output) { | |
133 DCHECK(foreground_runner_->RunsTasksOnCurrentThread()); | |
134 | |
135 DictionaryValue toplevel; | |
136 base::Time now = base::Time::Now(); | |
137 TransportSecurityState::Iterator state(*transport_security_state_); | |
138 for (; state.HasNext(); state.Advance()) { | |
139 const std::string& hostname = state.hostname(); | |
140 const TransportSecurityState::DomainState& domain_state = | |
141 state.domain_state(); | |
142 | |
143 DictionaryValue* serialized = new DictionaryValue; | |
144 serialized->SetBoolean(kStsIncludeSubdomains, | |
145 domain_state.sts_include_subdomains); | |
146 serialized->SetBoolean(kPkpIncludeSubdomains, | |
147 domain_state.pkp_include_subdomains); | |
148 serialized->SetDouble(kCreated, domain_state.created.ToDoubleT()); | |
149 serialized->SetDouble(kExpiry, domain_state.upgrade_expiry.ToDoubleT()); | |
150 serialized->SetDouble(kDynamicSPKIHashesExpiry, | |
151 domain_state.dynamic_spki_hashes_expiry.ToDoubleT()); | |
152 | |
153 switch (domain_state.upgrade_mode) { | |
154 case TransportSecurityState::DomainState::MODE_FORCE_HTTPS: | |
155 serialized->SetString(kMode, kForceHTTPS); | |
156 break; | |
157 case TransportSecurityState::DomainState::MODE_DEFAULT: | |
158 serialized->SetString(kMode, kDefault); | |
159 break; | |
160 default: | |
161 NOTREACHED() << "DomainState with unknown mode"; | |
162 delete serialized; | |
163 continue; | |
164 } | |
165 | |
166 serialized->Set(kStaticSPKIHashes, | |
167 SPKIHashesToListValue(domain_state.static_spki_hashes)); | |
168 | |
169 if (now < domain_state.dynamic_spki_hashes_expiry) { | |
170 serialized->Set(kDynamicSPKIHashes, | |
171 SPKIHashesToListValue(domain_state.dynamic_spki_hashes)); | |
172 } | |
173 | |
174 toplevel.Set(HashedDomainToExternalString(hostname), serialized); | |
175 } | |
176 | |
177 base::JSONWriter::WriteWithOptions(&toplevel, | |
178 base::JSONWriter::OPTIONS_PRETTY_PRINT, | |
179 output); | |
180 return true; | |
181 } | |
182 | |
183 bool TransportSecurityPersister::LoadEntries(const std::string& serialized, | |
184 bool* dirty) { | |
185 DCHECK(foreground_runner_->RunsTasksOnCurrentThread()); | |
186 | |
187 transport_security_state_->ClearDynamicData(); | |
188 return Deserialize(serialized, dirty, transport_security_state_); | |
189 } | |
190 | |
191 // static | |
192 bool TransportSecurityPersister::Deserialize(const std::string& serialized, | |
193 bool* dirty, | |
194 TransportSecurityState* state) { | |
195 scoped_ptr<Value> value(base::JSONReader::Read(serialized)); | |
196 DictionaryValue* dict_value = NULL; | |
197 if (!value.get() || !value->GetAsDictionary(&dict_value)) | |
198 return false; | |
199 | |
200 const base::Time current_time(base::Time::Now()); | |
201 bool dirtied = false; | |
202 | |
203 for (DictionaryValue::Iterator i(*dict_value); !i.IsAtEnd(); i.Advance()) { | |
204 const DictionaryValue* parsed = NULL; | |
205 if (!i.value().GetAsDictionary(&parsed)) { | |
206 LOG(WARNING) << "Could not parse entry " << i.key() << "; skipping entry"; | |
207 continue; | |
208 } | |
209 | |
210 std::string mode_string; | |
211 double created; | |
212 double expiry; | |
213 double dynamic_spki_hashes_expiry = 0.0; | |
214 TransportSecurityState::DomainState domain_state; | |
215 | |
216 // kIncludeSubdomains is a legacy synonym for kStsIncludeSubdomains and | |
217 // kPkpIncludeSubdomains. Parse at least one of these properties, | |
218 // preferably the new ones. | |
219 bool include_subdomains = false; | |
220 bool parsed_include_subdomains = parsed->GetBoolean(kIncludeSubdomains, | |
221 &include_subdomains); | |
222 domain_state.sts_include_subdomains = include_subdomains; | |
223 domain_state.pkp_include_subdomains = include_subdomains; | |
224 if (parsed->GetBoolean(kStsIncludeSubdomains, &include_subdomains)) { | |
225 domain_state.sts_include_subdomains = include_subdomains; | |
226 parsed_include_subdomains = true; | |
227 } | |
228 if (parsed->GetBoolean(kPkpIncludeSubdomains, &include_subdomains)) { | |
229 domain_state.pkp_include_subdomains = include_subdomains; | |
230 parsed_include_subdomains = true; | |
231 } | |
232 | |
233 if (!parsed_include_subdomains || | |
234 !parsed->GetString(kMode, &mode_string) || | |
235 !parsed->GetDouble(kExpiry, &expiry)) { | |
236 LOG(WARNING) << "Could not parse some elements of entry " << i.key() | |
237 << "; skipping entry"; | |
238 continue; | |
239 } | |
240 | |
241 // Don't fail if this key is not present. | |
242 parsed->GetDouble(kDynamicSPKIHashesExpiry, | |
243 &dynamic_spki_hashes_expiry); | |
244 | |
245 const ListValue* pins_list = NULL; | |
246 // preloaded_spki_hashes is a legacy synonym for static_spki_hashes. | |
247 if (parsed->GetList(kStaticSPKIHashes, &pins_list)) | |
248 SPKIHashesFromListValue(*pins_list, &domain_state.static_spki_hashes); | |
249 else if (parsed->GetList(kPreloadedSPKIHashes, &pins_list)) | |
250 SPKIHashesFromListValue(*pins_list, &domain_state.static_spki_hashes); | |
251 | |
252 if (parsed->GetList(kDynamicSPKIHashes, &pins_list)) | |
253 SPKIHashesFromListValue(*pins_list, &domain_state.dynamic_spki_hashes); | |
254 | |
255 if (mode_string == kForceHTTPS || mode_string == kStrict) { | |
256 domain_state.upgrade_mode = | |
257 TransportSecurityState::DomainState::MODE_FORCE_HTTPS; | |
258 } else if (mode_string == kDefault || mode_string == kPinningOnly) { | |
259 domain_state.upgrade_mode = | |
260 TransportSecurityState::DomainState::MODE_DEFAULT; | |
261 } else { | |
262 LOG(WARNING) << "Unknown TransportSecurityState mode string " | |
263 << mode_string << " found for entry " << i.key() | |
264 << "; skipping entry"; | |
265 continue; | |
266 } | |
267 | |
268 domain_state.upgrade_expiry = base::Time::FromDoubleT(expiry); | |
269 domain_state.dynamic_spki_hashes_expiry = | |
270 base::Time::FromDoubleT(dynamic_spki_hashes_expiry); | |
271 if (parsed->GetDouble(kCreated, &created)) { | |
272 domain_state.created = base::Time::FromDoubleT(created); | |
273 } else { | |
274 // We're migrating an old entry with no creation date. Make sure we | |
275 // write the new date back in a reasonable time frame. | |
276 dirtied = true; | |
277 domain_state.created = base::Time::Now(); | |
278 } | |
279 | |
280 if (domain_state.upgrade_expiry <= current_time && | |
281 domain_state.dynamic_spki_hashes_expiry <= current_time) { | |
282 // Make sure we dirty the state if we drop an entry. | |
283 dirtied = true; | |
284 continue; | |
285 } | |
286 | |
287 std::string hashed = ExternalStringToHashedDomain(i.key()); | |
288 if (hashed.empty()) { | |
289 dirtied = true; | |
290 continue; | |
291 } | |
292 | |
293 state->AddOrUpdateEnabledHosts(hashed, domain_state); | |
294 } | |
295 | |
296 *dirty = dirtied; | |
297 return true; | |
298 } | |
299 | |
300 void TransportSecurityPersister::CompleteLoad(const std::string& state) { | |
301 DCHECK(foreground_runner_->RunsTasksOnCurrentThread()); | |
302 | |
303 bool dirty = false; | |
304 if (!LoadEntries(state, &dirty)) { | |
305 LOG(ERROR) << "Failed to deserialize state: " << state; | |
306 return; | |
307 } | |
308 if (dirty) | |
309 StateIsDirty(transport_security_state_); | |
310 } | |
OLD | NEW |