Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(249)

Side by Side Diff: chrome/browser/ssl/chrome_ssl_host_state_decisions.cc

Issue 369703002: Remember user decisions on invalid certificates behind a flag (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Updated with experimental groups Created 6 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2014 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/ssl/chrome_ssl_host_state_decisions.h"
6
7 #include "base/base64.h"
8 #include "base/command_line.h"
9 #include "base/logging.h"
10 #include "base/metrics/field_trial.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/time/default_clock.h"
13 #include "base/time/time.h"
14 #include "chrome/browser/content_settings/host_content_settings_map.h"
15 #include "chrome/browser/profiles/profile.h"
16 #include "chrome/common/chrome_switches.h"
17 #include "components/content_settings/core/common/content_settings_types.h"
18 #include "components/variations/variations_associated_data.h"
19 #include "net/base/hash_value.h"
20 #include "net/cert/x509_certificate.h"
21 #include "url/gurl.h"
22
23 namespace {
24
25 // Switch value that specifies that certificate decisions should be forgotten at
26 // the end of the current session.
27 const int64 kForgetAtSessionEndSwitchValue = -1;
28
29 // Experiment information
30 const char kRememberCertificateErrorDecisionsFieldTrialName[] =
31 "RememberCertificateErrorDecisions";
32 const char kRememberCertificateErrorDecisionsFieldTrialDefaultGroup[] =
33 "Default";
34 const char kRememberCertificateErrorDecisionsFieldTrialLengthParam[] = "length";
35
36 // Keys for the per-site error + certificate finger to judgement content
37 // settings map.
38 const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map";
39 const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time";
40 const char kSSLCertDecisionVersionKey[] = "version";
41
42 const int kDefaultSSLCertDecisionVersion = 1;
43
44 // This is a helper function that returns the length of time before a
45 // certificate decision expires based on the command line flags. Returns a
46 // non-negative value in seconds or a value of -1 indicating that decisions
47 // should not be remembered after the current session has ended (but should be
48 // remembered indefinitely as long as the session does not end), which is the
49 // "old" style of certificate decision memory. Uses the experimental group
50 // unless overridden by a command line flag.
51 int64 GetExpirationDelta() {
52 // Check command line flags first to give them priority, then check
53 // experimental groups.
54 if (CommandLine::ForCurrentProcess()->HasSwitch(
55 switches::kRememberCertErrorDecisions)) {
56 std::string switch_value =
57 CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
58 switches::kRememberCertErrorDecisions);
59 int64 expiration_delta;
60 if (!base::StringToInt64(base::StringPiece(switch_value),
61 &expiration_delta) ||
62 expiration_delta < kForgetAtSessionEndSwitchValue) {
63 LOG(ERROR) << "Failed to parse the certificate error decision "
64 << "memory length: " << switch_value;
65 return kForgetAtSessionEndSwitchValue;
66 }
67
68 return expiration_delta;
69 }
Ryan Sleevi 2014/07/31 00:31:28 Is this switch necessary, given that we can force
jww 2014/07/31 05:57:00 Yes, it's necessary because we provide flags in ch
70
71 // If the user is in the field trial, set the expiration to the length
72 // associated with that experimental group. The default group cannot have
73 // parameters associated with it, so it needs to be handled explictly.
74 std::string group_name = base::FieldTrialList::FindFullName(
75 kRememberCertificateErrorDecisionsFieldTrialName);
76 if (!group_name.empty() &&
77 group_name.compare(
78 kRememberCertificateErrorDecisionsFieldTrialDefaultGroup) != 0) {
79 int64 field_trial_param_length;
80 std::string param = variations::GetVariationParamValue(
81 kRememberCertificateErrorDecisionsFieldTrialName,
82 kRememberCertificateErrorDecisionsFieldTrialLengthParam);
83 if (!param.empty() && base::StringToInt64(base::StringPiece(param),
84 &field_trial_param_length)) {
85 return field_trial_param_length;
86 }
87 }
88
89 return kForgetAtSessionEndSwitchValue;
90 }
91
92 std::string GetKey(net::X509Certificate* cert, net::CertStatus error) {
93 // Since a security decision will be made based on the fingerprint, Chrome
94 // should use the SHA-256 fingerprint for the certificate.
95 net::SHA256HashValue fingerprint =
96 net::X509Certificate::CalculateChainFingerprint256(
97 cert->os_cert_handle(), cert->GetIntermediateCertificates());
98 std::string base64_fingerprint;
99 base::Base64Encode(
100 base::StringPiece(reinterpret_cast<const char*>(fingerprint.data),
101 sizeof(fingerprint.data)),
102 &base64_fingerprint);
103 return base::UintToString(error) + base64_fingerprint;
104 }
105
106 } // namespace
107
108 // This helper function gets the dictionary of certificate fingerprints to
109 // errors of certificates that have been accepted by the user from the content
110 // dictionary that has been passed in. The returned pointer is owned by the the
111 // argument dict that is passed in.
112 //
113 // If create_entries is set to |DoNotCreateDictionaryEntries|,
114 // GetValidCertDecisionsDict will return NULL if there is anything invalid about
115 // the setting, such as an invalid version or invalid value types (in addition
116 // to there not be any values in the dictionary). If create_entries is set to
117 // |CreateDictionaryEntries|, if no dictionary is found or the decisions are
118 // expired, a new dictionary will be created
119 base::DictionaryValue* ChromeSSLHostStateDecisions::GetValidCertDecisionsDict(
120 base::DictionaryValue* dict,
121 CreateDictionaryEntriesDisposition create_entries) {
122 // Extract the version of the certificate decision structure from the content
123 // setting.
124 int version;
125 bool success = dict->GetInteger(kSSLCertDecisionVersionKey, &version);
126 if (!success) {
127 if (create_entries == DoNotCreateDictionaryEntries)
128 return NULL;
129
130 dict->SetInteger(kSSLCertDecisionVersionKey,
131 kDefaultSSLCertDecisionVersion);
132 version = kDefaultSSLCertDecisionVersion;
133 }
134
135 // If the version is somehow a newer version than Chrome can handle, there's
136 // really nothing to do other than fail silently and pretend it doesn't exist
137 // (or is malformed).
138 if (version > kDefaultSSLCertDecisionVersion) {
139 LOG(ERROR) << "Failed to parse a certificate error exception that is in a "
140 << "newer version format (" << version << ") than is supported ("
141 << kDefaultSSLCertDecisionVersion << ")";
142 return NULL;
143 }
144
145 // Extract the certificate decision's expiration time from the content
146 // setting. If there is no expiration time, that means it should never expire
147 // and it should reset only at session restart, so skip all of the expiration
148 // checks.
149 bool expired = false;
150 base::Time now = clock_->Now();
151 base::Time decision_expiration;
152 if (dict->HasKey(kSSLCertDecisionExpirationTimeKey)) {
153 std::string decision_expiration_string;
154 int64 decision_expiration_int64;
155 success = dict->GetString(kSSLCertDecisionExpirationTimeKey,
156 &decision_expiration_string);
157 if (!base::StringToInt64(base::StringPiece(decision_expiration_string),
158 &decision_expiration_int64)) {
159 LOG(ERROR) << "Failed to parse a certificate error exception that has a "
160 << "bad value for an expiration time: "
161 << decision_expiration_string;
162 return NULL;
163 }
164 decision_expiration =
165 base::Time::FromInternalValue(decision_expiration_int64);
166 }
167
168 // Check to see if the user's certificate decision has expired.
169 // - Expired and |create_entries| is DoNotCreateDictionaryEntries, return
170 // NULL.
171 // - Expired and |create_entries| is CreateDictionaryEntries, update the
172 // expiration time.
173 if (should_remember_ssl_decisions_ !=
174 ForgetSSLExceptionDecisionsAtSessionEnd &&
175 decision_expiration.ToInternalValue() <= now.ToInternalValue()) {
176 if (create_entries == DoNotCreateDictionaryEntries)
177 return NULL;
178
179 expired = true;
180
181 base::Time expiration_time =
182 now + default_ssl_cert_decision_expiration_delta_;
183 // Unfortunately, JSON (and thus content settings) doesn't support int64
184 // values, only doubles. Since this mildly depends on precision, it is
185 // better to store the value as a string.
186 dict->SetString(kSSLCertDecisionExpirationTimeKey,
187 base::Int64ToString(expiration_time.ToInternalValue()));
188 }
189
190 // Extract the map of certificate fingerprints to errors from the setting.
191 base::DictionaryValue* cert_error_dict; // Will be owned by dict
192 if (expired ||
193 !dict->GetDictionary(kSSLCertDecisionCertErrorMapKey, &cert_error_dict)) {
194 if (create_entries == DoNotCreateDictionaryEntries)
195 return NULL;
196
197 cert_error_dict = new base::DictionaryValue();
198 // dict takes ownership of cert_error_dict
199 dict->Set(kSSLCertDecisionCertErrorMapKey, cert_error_dict);
200 }
201
202 return cert_error_dict;
203 }
204
205 // If |should_remember_ssl_decisions_| is
206 // ForgetSSLExceptionDecisionsAtSessionEnd, that means that all invalid
207 // certificate proceed decisions should be forgotten when the session ends. At
208 // attempt is made in the destructor to remove the entries, but in the case that
209 // things didn't shut down cleanly, on start, Clear is called to guarantee a
210 // clean state.
211 ChromeSSLHostStateDecisions::ChromeSSLHostStateDecisions(Profile* profile)
212 : clock_(new base::DefaultClock()), profile_(profile) {
213 int64 expiration_delta = GetExpirationDelta();
214 if (expiration_delta == kForgetAtSessionEndSwitchValue) {
215 should_remember_ssl_decisions_ = ForgetSSLExceptionDecisionsAtSessionEnd;
216 expiration_delta = 0;
217 Clear();
218 } else {
219 should_remember_ssl_decisions_ = RememberSSLExceptionDecisionsForDelta;
220 }
221 default_ssl_cert_decision_expiration_delta_ =
222 base::TimeDelta::FromSeconds(expiration_delta);
223 }
224
225 ChromeSSLHostStateDecisions::~ChromeSSLHostStateDecisions() {
226 if (should_remember_ssl_decisions_ == ForgetSSLExceptionDecisionsAtSessionEnd)
227 Clear();
228 }
229
230 void ChromeSSLHostStateDecisions::DenyCert(const GURL& url,
231 net::X509Certificate* cert,
232 net::CertStatus error) {
233 ChangeCertPolicy(url, cert, error, net::CertPolicy::DENIED);
234 }
235
236 void ChromeSSLHostStateDecisions::AllowCert(const GURL& url,
237 net::X509Certificate* cert,
238 net::CertStatus error) {
239 ChangeCertPolicy(url, cert, error, net::CertPolicy::ALLOWED);
240 }
241
242 void ChromeSSLHostStateDecisions::Clear() {
243 profile_->GetHostContentSettingsMap()->ClearSettingsForOneType(
244 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS);
245 }
246
247 net::CertPolicy::Judgment ChromeSSLHostStateDecisions::QueryPolicy(
248 const GURL& url,
249 net::X509Certificate* cert,
250 net::CertStatus error) {
251 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
252 scoped_ptr<base::Value> value(
253 map->GetWebsiteSetting(url.GetWithEmptyPath(),
254 url.GetWithEmptyPath(),
255 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
256 std::string(),
257 NULL));
258
259 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
260 return net::CertPolicy::UNKNOWN;
261
262 base::DictionaryValue* dict; // Owned by value
263 int policy_decision;
264 bool success = value->GetAsDictionary(&dict);
265 DCHECK(success);
266
267 base::DictionaryValue* cert_error_dict; // Owned by value
268 cert_error_dict =
269 GetValidCertDecisionsDict(dict, DoNotCreateDictionaryEntries);
270 if (!cert_error_dict)
271 return net::CertPolicy::UNKNOWN;
272
273 success = cert_error_dict->GetIntegerWithoutPathExpansion(GetKey(cert, error),
274 &policy_decision);
275
276 if (!success)
277 return net::CertPolicy::Judgment::UNKNOWN;
278
279 return static_cast<net::CertPolicy::Judgment>(policy_decision);
Ryan Sleevi 2014/07/31 00:31:28 SECURITY: Seems like you should bounds check this,
jww 2014/07/31 05:57:00 Done.
280 }
281
282 void ChromeSSLHostStateDecisions::RevokeAllowAndDenyPreferences(
283 const GURL& url) {
284 const ContentSettingsPattern pattern =
285 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
286 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
287
288 map->SetWebsiteSetting(pattern,
289 pattern,
290 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
291 std::string(),
292 NULL);
293 }
294
295 bool ChromeSSLHostStateDecisions::HasAllowedOrDeniedCert(const GURL& url) {
296 const ContentSettingsPattern pattern =
297 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
298 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
299
300 scoped_ptr<base::Value> value(
301 map->GetWebsiteSetting(url.GetWithEmptyPath(),
302 url.GetWithEmptyPath(),
303 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
304 std::string(),
305 NULL));
306
307 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
308 return false;
309
310 base::DictionaryValue* dict; // Owned by value
311 bool success = value->GetAsDictionary(&dict);
312 DCHECK(success);
313
314 for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) {
315 int policy_decision; // Owned by dict
316 success = it.value().GetAsInteger(&policy_decision);
317 if (success && (static_cast<net::CertPolicy::Judgment>(policy_decision) !=
318 net::CertPolicy::UNKNOWN))
319 return true;
320 }
321
322 return false;
323 }
324
325 void ChromeSSLHostStateDecisions::ChangeCertPolicy(
326 const GURL& url,
327 net::X509Certificate* cert,
328 net::CertStatus error,
329 net::CertPolicy::Judgment judgment) {
330 const ContentSettingsPattern pattern =
331 ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
332 HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
333 scoped_ptr<base::Value> value(
334 map->GetWebsiteSetting(url.GetWithEmptyPath(),
335 url.GetWithEmptyPath(),
336 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
337 std::string(),
338 NULL));
339
340 if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
341 value.reset(new base::DictionaryValue());
342
343 base::DictionaryValue* dict;
344 bool success = value->GetAsDictionary(&dict);
345 DCHECK(success);
346
347 base::DictionaryValue* cert_dict =
348 GetValidCertDecisionsDict(dict, CreateDictionaryEntries);
349 // If a a valid certificate dictionary cannot be extracted from the content
350 // setting, that means it's in an unknown format. Unfortunately, there's
351 // nothing to be done in that case, so a silent fail is the only option.
352 if (!cert_dict)
353 return;
354
355 dict->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey,
356 kDefaultSSLCertDecisionVersion);
357 cert_dict->SetIntegerWithoutPathExpansion(GetKey(cert, error), judgment);
358
359 // The map takes ownership of the value, so it is released in the call to
360 // SetWebsiteSetting.
361 map->SetWebsiteSetting(pattern,
362 pattern,
363 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
364 std::string(),
365 value.release());
366 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698