1 // Copyright 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.
5 #include "chrome/browser/ssl/chrome_ssl_host_state_delegate.h"
9 #include "base/base64.h"
10 #include "base/bind.h"
11 #include "base/command_line.h"
12 #include "base/guid.h"
13 #include "base/logging.h"
14 #include "base/metrics/field_trial.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "base/time/clock.h"
17 #include "base/time/default_clock.h"
18 #include "base/time/time.h"
19 #include "base/values.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/common/chrome_switches.h"
22 #include "components/content_settings/core/browser/host_content_settings_map.h"
23 #include "components/content_settings/core/common/content_settings_types.h"
24 #include "components/variations/variations_associated_data.h"
25 #include "net/base/hash_value.h"
26 #include "net/base/net_util.h"
27 #include "net/cert/x509_certificate.h"
28 #include "net/http/http_transaction_factory.h"
29 #include "net/url_request/url_request_context.h"
30 #include "net/url_request/url_request_context_getter.h"
35 // Switch value that specifies that certificate decisions should be forgotten at
36 // the end of the current session.
37 const int64 kForgetAtSessionEndSwitchValue
= -1;
39 // Experiment information
40 const char kRememberCertificateErrorDecisionsFieldTrialName
[] =
41 "RememberCertificateErrorDecisions";
42 const char kRememberCertificateErrorDecisionsFieldTrialDefaultGroup
[] =
44 const char kRememberCertificateErrorDecisionsFieldTrialLengthParam
[] = "length";
46 // Keys for the per-site error + certificate finger to judgment content
48 const char kSSLCertDecisionCertErrorMapKey
[] = "cert_exceptions_map";
49 const char kSSLCertDecisionExpirationTimeKey
[] = "decision_expiration_time";
50 const char kSSLCertDecisionVersionKey
[] = "version";
51 const char kSSLCertDecisionGUIDKey
[] = "guid";
53 const int kDefaultSSLCertDecisionVersion
= 1;
55 void CloseIdleConnections(
56 scoped_refptr
<net::URLRequestContextGetter
> url_request_context_getter
) {
57 url_request_context_getter
->
58 GetURLRequestContext()->
59 http_transaction_factory()->
61 CloseIdleConnections();
64 // All SSL decisions are per host (and are shared arcoss schemes), so this
65 // canonicalizes all hosts into a secure scheme GURL to use with content
66 // settings. The returned GURL will be the passed in host with an empty path and
67 // https:// as the scheme.
68 GURL
GetSecureGURLForHost(const std::string
& host
) {
69 std::string url
= "https://" + host
;
73 // This is a helper function that returns the length of time before a
74 // certificate decision expires based on the command line flags. Returns a
75 // non-negative value in seconds or a value of -1 indicating that decisions
76 // should not be remembered after the current session has ended (but should be
77 // remembered indefinitely as long as the session does not end), which is the
78 // "old" style of certificate decision memory. Uses the experimental group
79 // unless overridden by a command line flag.
80 int64
GetExpirationDelta() {
81 // Check command line flags first to give them priority, then check
82 // experimental groups.
83 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
84 switches::kRememberCertErrorDecisions
)) {
85 std::string switch_value
=
86 base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
87 switches::kRememberCertErrorDecisions
);
88 int64 expiration_delta
;
89 if (!base::StringToInt64(base::StringPiece(switch_value
),
91 expiration_delta
< kForgetAtSessionEndSwitchValue
) {
92 LOG(ERROR
) << "Failed to parse the certificate error decision "
93 << "memory length: " << switch_value
;
94 return kForgetAtSessionEndSwitchValue
;
97 return expiration_delta
;
100 // If the user is in the field trial, set the expiration to the length
101 // associated with that experimental group. The default group cannot have
102 // parameters associated with it, so it needs to be handled explictly.
103 std::string group_name
= base::FieldTrialList::FindFullName(
104 kRememberCertificateErrorDecisionsFieldTrialName
);
105 if (!group_name
.empty() &&
107 kRememberCertificateErrorDecisionsFieldTrialDefaultGroup
) != 0) {
108 int64 field_trial_param_length
;
109 std::string param
= variations::GetVariationParamValue(
110 kRememberCertificateErrorDecisionsFieldTrialName
,
111 kRememberCertificateErrorDecisionsFieldTrialLengthParam
);
112 if (!param
.empty() && base::StringToInt64(base::StringPiece(param
),
113 &field_trial_param_length
)) {
114 return field_trial_param_length
;
118 return kForgetAtSessionEndSwitchValue
;
121 std::string
GetKey(const net::X509Certificate
& cert
, net::CertStatus error
) {
122 // Since a security decision will be made based on the fingerprint, Chrome
123 // should use the SHA-256 fingerprint for the certificate.
124 net::SHA256HashValue fingerprint
=
125 net::X509Certificate::CalculateChainFingerprint256(
126 cert
.os_cert_handle(), cert
.GetIntermediateCertificates());
127 std::string base64_fingerprint
;
129 base::StringPiece(reinterpret_cast<const char*>(fingerprint
.data
),
130 sizeof(fingerprint
.data
)),
131 &base64_fingerprint
);
132 return base::UintToString(error
) + base64_fingerprint
;
137 // This helper function gets the dictionary of certificate fingerprints to
138 // errors of certificates that have been accepted by the user from the content
139 // dictionary that has been passed in. The returned pointer is owned by the the
140 // argument dict that is passed in.
142 // If create_entries is set to |DO_NOT_CREATE_DICTIONARY_ENTRIES|,
143 // GetValidCertDecisionsDict will return NULL if there is anything invalid about
144 // the setting, such as an invalid version or invalid value types (in addition
145 // to there not being any values in the dictionary). If create_entries is set to
146 // |CREATE_DICTIONARY_ENTRIES|, if no dictionary is found or the decisions are
147 // expired, a new dictionary will be created.
148 base::DictionaryValue
* ChromeSSLHostStateDelegate::GetValidCertDecisionsDict(
149 base::DictionaryValue
* dict
,
150 CreateDictionaryEntriesDisposition create_entries
,
151 bool* expired_previous_decision
) {
152 // This needs to be done first in case the method is short circuited by an
154 *expired_previous_decision
= false;
156 // Extract the version of the certificate decision structure from the content
159 bool success
= dict
->GetInteger(kSSLCertDecisionVersionKey
, &version
);
161 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
164 dict
->SetInteger(kSSLCertDecisionVersionKey
,
165 kDefaultSSLCertDecisionVersion
);
166 version
= kDefaultSSLCertDecisionVersion
;
169 // If the version is somehow a newer version than Chrome can handle, there's
170 // really nothing to do other than fail silently and pretend it doesn't exist
171 // (or is malformed).
172 if (version
> kDefaultSSLCertDecisionVersion
) {
173 LOG(ERROR
) << "Failed to parse a certificate error exception that is in a "
174 << "newer version format (" << version
<< ") than is supported ("
175 << kDefaultSSLCertDecisionVersion
<< ")";
179 // Extract the certificate decision's expiration time from the content
180 // setting. If there is no expiration time, that means it should never expire
181 // and it should reset only at session restart, so skip all of the expiration
183 bool expired
= false;
184 base::Time now
= clock_
->Now();
185 base::Time decision_expiration
;
186 if (dict
->HasKey(kSSLCertDecisionExpirationTimeKey
)) {
187 std::string decision_expiration_string
;
188 int64 decision_expiration_int64
;
189 success
= dict
->GetString(kSSLCertDecisionExpirationTimeKey
,
190 &decision_expiration_string
);
191 if (!base::StringToInt64(base::StringPiece(decision_expiration_string
),
192 &decision_expiration_int64
)) {
193 LOG(ERROR
) << "Failed to parse a certificate error exception that has a "
194 << "bad value for an expiration time: "
195 << decision_expiration_string
;
198 decision_expiration
=
199 base::Time::FromInternalValue(decision_expiration_int64
);
202 // Check to see if the user's certificate decision has expired.
203 // - Expired and |create_entries| is DO_NOT_CREATE_DICTIONARY_ENTRIES, return
205 // - Expired and |create_entries| is CREATE_DICTIONARY_ENTRIES, update the
207 if (should_remember_ssl_decisions_
!=
208 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
&&
209 decision_expiration
.ToInternalValue() <= now
.ToInternalValue()) {
210 *expired_previous_decision
= true;
212 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
216 base::Time expiration_time
=
217 now
+ default_ssl_cert_decision_expiration_delta_
;
218 // Unfortunately, JSON (and thus content settings) doesn't support int64
219 // values, only doubles. Since this mildly depends on precision, it is
220 // better to store the value as a string.
221 dict
->SetString(kSSLCertDecisionExpirationTimeKey
,
222 base::Int64ToString(expiration_time
.ToInternalValue()));
223 } else if (should_remember_ssl_decisions_
==
224 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
) {
225 if (dict
->HasKey(kSSLCertDecisionGUIDKey
)) {
226 std::string old_expiration_guid
;
227 success
= dict
->GetString(kSSLCertDecisionGUIDKey
, &old_expiration_guid
);
228 if (old_expiration_guid
.compare(current_expiration_guid_
) != 0) {
229 *expired_previous_decision
= true;
235 dict
->SetString(kSSLCertDecisionGUIDKey
, current_expiration_guid_
);
237 // Extract the map of certificate fingerprints to errors from the setting.
238 base::DictionaryValue
* cert_error_dict
= NULL
; // Will be owned by dict
240 !dict
->GetDictionary(kSSLCertDecisionCertErrorMapKey
, &cert_error_dict
)) {
241 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
244 cert_error_dict
= new base::DictionaryValue();
245 // dict takes ownership of cert_error_dict
246 dict
->Set(kSSLCertDecisionCertErrorMapKey
, cert_error_dict
);
249 return cert_error_dict
;
252 // If |should_remember_ssl_decisions_| is
253 // FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END, that means that all invalid
254 // certificate proceed decisions should be forgotten when the session ends. To
255 // simulate that, Chrome keeps track of a guid to represent the current browser
256 // session and stores it in decision entries. See the comment for
257 // |current_expiration_guid_| for more information.
258 ChromeSSLHostStateDelegate::ChromeSSLHostStateDelegate(Profile
* profile
)
259 : clock_(new base::DefaultClock()),
261 current_expiration_guid_(base::GenerateGUID()) {
262 int64 expiration_delta
= GetExpirationDelta();
263 if (expiration_delta
== kForgetAtSessionEndSwitchValue
) {
264 should_remember_ssl_decisions_
=
265 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
;
266 expiration_delta
= 0;
268 should_remember_ssl_decisions_
= REMEMBER_SSL_EXCEPTION_DECISIONS_FOR_DELTA
;
270 default_ssl_cert_decision_expiration_delta_
=
271 base::TimeDelta::FromSeconds(expiration_delta
);
274 ChromeSSLHostStateDelegate::~ChromeSSLHostStateDelegate() {
277 void ChromeSSLHostStateDelegate::AllowCert(const std::string
& host
,
278 const net::X509Certificate
& cert
,
279 net::CertStatus error
) {
280 GURL url
= GetSecureGURLForHost(host
);
281 const ContentSettingsPattern pattern
=
282 ContentSettingsPattern::FromURLNoWildcard(url
);
283 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
284 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
285 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
287 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
288 value
.reset(new base::DictionaryValue());
290 base::DictionaryValue
* dict
;
291 bool success
= value
->GetAsDictionary(&dict
);
294 bool expired_previous_decision
; // unused value in this function
295 base::DictionaryValue
* cert_dict
= GetValidCertDecisionsDict(
296 dict
, CREATE_DICTIONARY_ENTRIES
, &expired_previous_decision
);
297 // If a a valid certificate dictionary cannot be extracted from the content
298 // setting, that means it's in an unknown format. Unfortunately, there's
299 // nothing to be done in that case, so a silent fail is the only option.
303 dict
->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey
,
304 kDefaultSSLCertDecisionVersion
);
305 cert_dict
->SetIntegerWithoutPathExpansion(GetKey(cert
, error
), ALLOWED
);
307 // The map takes ownership of the value, so it is released in the call to
308 // SetWebsiteSetting.
309 map
->SetWebsiteSetting(pattern
,
311 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
,
316 void ChromeSSLHostStateDelegate::Clear() {
317 profile_
->GetHostContentSettingsMap()->ClearSettingsForOneType(
318 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
);
321 content::SSLHostStateDelegate::CertJudgment
322 ChromeSSLHostStateDelegate::QueryPolicy(const std::string
& host
,
323 const net::X509Certificate
& cert
,
324 net::CertStatus error
,
325 bool* expired_previous_decision
) {
326 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
327 GURL url
= GetSecureGURLForHost(host
);
328 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
329 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
331 // Set a default value in case this method is short circuited and doesn't do a
333 *expired_previous_decision
= false;
335 // If the appropriate flag is set, let requests on localhost go
336 // through even if there are certificate errors. Errors on localhost
337 // are unlikely to indicate actual security problems.
338 bool allow_localhost
= base::CommandLine::ForCurrentProcess()->HasSwitch(
339 switches::kAllowInsecureLocalhost
);
340 if (allow_localhost
&& net::IsLocalhost(url
.host()))
343 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
346 base::DictionaryValue
* dict
; // Owned by value
348 bool success
= value
->GetAsDictionary(&dict
);
351 base::DictionaryValue
* cert_error_dict
; // Owned by value
352 cert_error_dict
= GetValidCertDecisionsDict(
353 dict
, DO_NOT_CREATE_DICTIONARY_ENTRIES
, expired_previous_decision
);
354 if (!cert_error_dict
) {
355 // This revoke is necessary to clear any old expired setting that may be
356 // lingering in the case that an old decision expried.
357 RevokeUserAllowExceptions(host
);
361 success
= cert_error_dict
->GetIntegerWithoutPathExpansion(GetKey(cert
, error
),
364 // If a policy decision was successfully retrieved and it's a valid value of
365 // ALLOWED, return the valid value. Otherwise, return DENIED.
366 if (success
&& policy_decision
== ALLOWED
)
372 void ChromeSSLHostStateDelegate::RevokeUserAllowExceptions(
373 const std::string
& host
) {
374 GURL url
= GetSecureGURLForHost(host
);
375 const ContentSettingsPattern pattern
=
376 ContentSettingsPattern::FromURLNoWildcard(url
);
377 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
379 map
->SetWebsiteSetting(pattern
,
381 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
,
386 // TODO(jww): This will revoke all of the decisions in the browser context.
387 // However, the networking stack actually keeps track of its own list of
388 // exceptions per-HttpNetworkTransaction in the SSLConfig structure (see the
389 // allowed_bad_certs Vector in net/ssl/ssl_config.h). This dual-tracking of
390 // exceptions introduces a problem where the browser context can revoke a
391 // certificate, but if a transaction reuses a cached version of the SSLConfig
392 // (probably from a pooled socket), it may bypass the intestitial layer.
394 // Over time, the cached versions should expire and it should converge on
395 // showing the interstitial. We probably need to introduce into the networking
396 // stack a way revoke SSLConfig's allowed_bad_certs lists per socket.
398 // For now, RevokeUserAllowExceptionsHard is our solution for the rare case
399 // where it is necessary to revoke the preferences immediately. It does so by
400 // flushing idle sockets, thus it is a big hammer and should be wielded with
401 // extreme caution as it can have a big, negative impact on network performance.
402 void ChromeSSLHostStateDelegate::RevokeUserAllowExceptionsHard(
403 const std::string
& host
) {
404 RevokeUserAllowExceptions(host
);
405 scoped_refptr
<net::URLRequestContextGetter
> getter(
406 profile_
->GetRequestContext());
407 getter
->GetNetworkTaskRunner()->PostTask(
408 FROM_HERE
, base::Bind(&CloseIdleConnections
, getter
));
411 bool ChromeSSLHostStateDelegate::HasAllowException(
412 const std::string
& host
) const {
413 GURL url
= GetSecureGURLForHost(host
);
414 const ContentSettingsPattern pattern
=
415 ContentSettingsPattern::FromURLNoWildcard(url
);
416 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
418 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
419 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
421 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
424 base::DictionaryValue
* dict
; // Owned by value
425 bool success
= value
->GetAsDictionary(&dict
);
428 for (base::DictionaryValue::Iterator
it(*dict
); !it
.IsAtEnd(); it
.Advance()) {
429 int policy_decision
; // Owned by dict
430 success
= it
.value().GetAsInteger(&policy_decision
);
431 if (success
&& (static_cast<CertJudgment
>(policy_decision
) == ALLOWED
))
438 void ChromeSSLHostStateDelegate::HostRanInsecureContent(const std::string
& host
,
440 ran_insecure_content_hosts_
.insert(BrokenHostEntry(host
, pid
));
443 bool ChromeSSLHostStateDelegate::DidHostRunInsecureContent(
444 const std::string
& host
,
446 return !!ran_insecure_content_hosts_
.count(BrokenHostEntry(host
, pid
));
448 void ChromeSSLHostStateDelegate::SetClock(scoped_ptr
<base::Clock
> clock
) {
449 clock_
.reset(clock
.release());