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 // The default expiration is one week, unless overidden by a field trial group.
36 // See https://crbug.com/487270.
37 const uint64_t kDeltaDefaultExpirationInSeconds
= UINT64_C(604800);
39 // Field trial information
40 const char kRevertCertificateErrorDecisionsFieldTrialName
[] =
41 "RevertCertificateErrorDecisions";
42 const char kForgetAtSessionEndGroup
[] = "Session";
44 // Keys for the per-site error + certificate finger to judgment content
46 const char kSSLCertDecisionCertErrorMapKey
[] = "cert_exceptions_map";
47 const char kSSLCertDecisionExpirationTimeKey
[] = "decision_expiration_time";
48 const char kSSLCertDecisionVersionKey
[] = "version";
49 const char kSSLCertDecisionGUIDKey
[] = "guid";
51 const int kDefaultSSLCertDecisionVersion
= 1;
53 void CloseIdleConnections(
54 scoped_refptr
<net::URLRequestContextGetter
> url_request_context_getter
) {
55 url_request_context_getter
->
56 GetURLRequestContext()->
57 http_transaction_factory()->
59 CloseIdleConnections();
62 // All SSL decisions are per host (and are shared arcoss schemes), so this
63 // canonicalizes all hosts into a secure scheme GURL to use with content
64 // settings. The returned GURL will be the passed in host with an empty path and
65 // https:// as the scheme.
66 GURL
GetSecureGURLForHost(const std::string
& host
) {
67 std::string url
= "https://" + host
;
71 // By default, certificate exception decisions are remembered for one week.
72 // However, there is a field trial group for the "old" style of certificate
73 // decision memory that expires decisions at session end. ExpireAtSessionEnd()
74 // returns |true| if and only if the user is in that field trial group.
75 bool ExpireAtSessionEnd() {
76 std::string group_name
= base::FieldTrialList::FindFullName(
77 kRevertCertificateErrorDecisionsFieldTrialName
);
78 return !group_name
.empty() &&
79 group_name
.compare(kForgetAtSessionEndGroup
) == 0;
82 std::string
GetKey(const net::X509Certificate
& cert
, net::CertStatus error
) {
83 // Since a security decision will be made based on the fingerprint, Chrome
84 // should use the SHA-256 fingerprint for the certificate.
85 net::SHA256HashValue fingerprint
=
86 net::X509Certificate::CalculateChainFingerprint256(
87 cert
.os_cert_handle(), cert
.GetIntermediateCertificates());
88 std::string base64_fingerprint
;
90 base::StringPiece(reinterpret_cast<const char*>(fingerprint
.data
),
91 sizeof(fingerprint
.data
)),
93 return base::UintToString(error
) + base64_fingerprint
;
98 // This helper function gets the dictionary of certificate fingerprints to
99 // errors of certificates that have been accepted by the user from the content
100 // dictionary that has been passed in. The returned pointer is owned by the the
101 // argument dict that is passed in.
103 // If create_entries is set to |DO_NOT_CREATE_DICTIONARY_ENTRIES|,
104 // GetValidCertDecisionsDict will return NULL if there is anything invalid about
105 // the setting, such as an invalid version or invalid value types (in addition
106 // to there not being any values in the dictionary). If create_entries is set to
107 // |CREATE_DICTIONARY_ENTRIES|, if no dictionary is found or the decisions are
108 // expired, a new dictionary will be created.
109 base::DictionaryValue
* ChromeSSLHostStateDelegate::GetValidCertDecisionsDict(
110 base::DictionaryValue
* dict
,
111 CreateDictionaryEntriesDisposition create_entries
,
112 bool* expired_previous_decision
) {
113 // This needs to be done first in case the method is short circuited by an
115 *expired_previous_decision
= false;
117 // Extract the version of the certificate decision structure from the content
120 bool success
= dict
->GetInteger(kSSLCertDecisionVersionKey
, &version
);
122 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
125 dict
->SetInteger(kSSLCertDecisionVersionKey
,
126 kDefaultSSLCertDecisionVersion
);
127 version
= kDefaultSSLCertDecisionVersion
;
130 // If the version is somehow a newer version than Chrome can handle, there's
131 // really nothing to do other than fail silently and pretend it doesn't exist
132 // (or is malformed).
133 if (version
> kDefaultSSLCertDecisionVersion
) {
134 LOG(ERROR
) << "Failed to parse a certificate error exception that is in a "
135 << "newer version format (" << version
<< ") than is supported ("
136 << kDefaultSSLCertDecisionVersion
<< ")";
140 // Extract the certificate decision's expiration time from the content
141 // setting. If there is no expiration time, that means it should never expire
142 // and it should reset only at session restart, so skip all of the expiration
144 bool expired
= false;
145 base::Time now
= clock_
->Now();
146 base::Time decision_expiration
;
147 if (dict
->HasKey(kSSLCertDecisionExpirationTimeKey
)) {
148 std::string decision_expiration_string
;
149 int64 decision_expiration_int64
;
150 success
= dict
->GetString(kSSLCertDecisionExpirationTimeKey
,
151 &decision_expiration_string
);
152 if (!base::StringToInt64(base::StringPiece(decision_expiration_string
),
153 &decision_expiration_int64
)) {
154 LOG(ERROR
) << "Failed to parse a certificate error exception that has a "
155 << "bad value for an expiration time: "
156 << decision_expiration_string
;
159 decision_expiration
=
160 base::Time::FromInternalValue(decision_expiration_int64
);
163 // Check to see if the user's certificate decision has expired.
164 // - Expired and |create_entries| is DO_NOT_CREATE_DICTIONARY_ENTRIES, return
166 // - Expired and |create_entries| is CREATE_DICTIONARY_ENTRIES, update the
168 if (should_remember_ssl_decisions_
!=
169 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
&&
170 decision_expiration
.ToInternalValue() <= now
.ToInternalValue()) {
171 *expired_previous_decision
= true;
173 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
177 base::Time expiration_time
=
178 now
+ base::TimeDelta::FromSeconds(kDeltaDefaultExpirationInSeconds
);
179 // Unfortunately, JSON (and thus content settings) doesn't support int64
180 // values, only doubles. Since this mildly depends on precision, it is
181 // better to store the value as a string.
182 dict
->SetString(kSSLCertDecisionExpirationTimeKey
,
183 base::Int64ToString(expiration_time
.ToInternalValue()));
184 } else if (should_remember_ssl_decisions_
==
185 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
) {
186 if (dict
->HasKey(kSSLCertDecisionGUIDKey
)) {
187 std::string old_expiration_guid
;
188 success
= dict
->GetString(kSSLCertDecisionGUIDKey
, &old_expiration_guid
);
189 if (old_expiration_guid
.compare(current_expiration_guid_
) != 0) {
190 *expired_previous_decision
= true;
196 dict
->SetString(kSSLCertDecisionGUIDKey
, current_expiration_guid_
);
198 // Extract the map of certificate fingerprints to errors from the setting.
199 base::DictionaryValue
* cert_error_dict
= NULL
; // Will be owned by dict
201 !dict
->GetDictionary(kSSLCertDecisionCertErrorMapKey
, &cert_error_dict
)) {
202 if (create_entries
== DO_NOT_CREATE_DICTIONARY_ENTRIES
)
205 cert_error_dict
= new base::DictionaryValue();
206 // dict takes ownership of cert_error_dict
207 dict
->Set(kSSLCertDecisionCertErrorMapKey
, cert_error_dict
);
210 return cert_error_dict
;
213 // If |should_remember_ssl_decisions_| is
214 // FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END, that means that all invalid
215 // certificate proceed decisions should be forgotten when the session ends. To
216 // simulate that, Chrome keeps track of a guid to represent the current browser
217 // session and stores it in decision entries. See the comment for
218 // |current_expiration_guid_| for more information.
219 ChromeSSLHostStateDelegate::ChromeSSLHostStateDelegate(Profile
* profile
)
220 : clock_(new base::DefaultClock()),
222 current_expiration_guid_(base::GenerateGUID()) {
223 if (ExpireAtSessionEnd())
224 should_remember_ssl_decisions_
=
225 FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END
;
227 should_remember_ssl_decisions_
= REMEMBER_SSL_EXCEPTION_DECISIONS_FOR_DELTA
;
230 ChromeSSLHostStateDelegate::~ChromeSSLHostStateDelegate() {
233 void ChromeSSLHostStateDelegate::AllowCert(const std::string
& host
,
234 const net::X509Certificate
& cert
,
235 net::CertStatus error
) {
236 GURL url
= GetSecureGURLForHost(host
);
237 const ContentSettingsPattern pattern
=
238 ContentSettingsPattern::FromURLNoWildcard(url
);
239 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
240 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
241 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
243 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
244 value
.reset(new base::DictionaryValue());
246 base::DictionaryValue
* dict
;
247 bool success
= value
->GetAsDictionary(&dict
);
250 bool expired_previous_decision
; // unused value in this function
251 base::DictionaryValue
* cert_dict
= GetValidCertDecisionsDict(
252 dict
, CREATE_DICTIONARY_ENTRIES
, &expired_previous_decision
);
253 // If a a valid certificate dictionary cannot be extracted from the content
254 // setting, that means it's in an unknown format. Unfortunately, there's
255 // nothing to be done in that case, so a silent fail is the only option.
259 dict
->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey
,
260 kDefaultSSLCertDecisionVersion
);
261 cert_dict
->SetIntegerWithoutPathExpansion(GetKey(cert
, error
), ALLOWED
);
263 // The map takes ownership of the value, so it is released in the call to
264 // SetWebsiteSetting.
265 map
->SetWebsiteSetting(pattern
,
267 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
,
272 void ChromeSSLHostStateDelegate::Clear() {
273 profile_
->GetHostContentSettingsMap()->ClearSettingsForOneType(
274 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
);
277 content::SSLHostStateDelegate::CertJudgment
278 ChromeSSLHostStateDelegate::QueryPolicy(const std::string
& host
,
279 const net::X509Certificate
& cert
,
280 net::CertStatus error
,
281 bool* expired_previous_decision
) {
282 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
283 GURL url
= GetSecureGURLForHost(host
);
284 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
285 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
287 // Set a default value in case this method is short circuited and doesn't do a
289 *expired_previous_decision
= false;
291 // If the appropriate flag is set, let requests on localhost go
292 // through even if there are certificate errors. Errors on localhost
293 // are unlikely to indicate actual security problems.
294 bool allow_localhost
= base::CommandLine::ForCurrentProcess()->HasSwitch(
295 switches::kAllowInsecureLocalhost
);
296 if (allow_localhost
&& net::IsLocalhost(url
.host()))
299 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
302 base::DictionaryValue
* dict
; // Owned by value
304 bool success
= value
->GetAsDictionary(&dict
);
307 base::DictionaryValue
* cert_error_dict
; // Owned by value
308 cert_error_dict
= GetValidCertDecisionsDict(
309 dict
, DO_NOT_CREATE_DICTIONARY_ENTRIES
, expired_previous_decision
);
310 if (!cert_error_dict
) {
311 // This revoke is necessary to clear any old expired setting that may be
312 // lingering in the case that an old decision expried.
313 RevokeUserAllowExceptions(host
);
317 success
= cert_error_dict
->GetIntegerWithoutPathExpansion(GetKey(cert
, error
),
320 // If a policy decision was successfully retrieved and it's a valid value of
321 // ALLOWED, return the valid value. Otherwise, return DENIED.
322 if (success
&& policy_decision
== ALLOWED
)
328 void ChromeSSLHostStateDelegate::RevokeUserAllowExceptions(
329 const std::string
& host
) {
330 GURL url
= GetSecureGURLForHost(host
);
331 const ContentSettingsPattern pattern
=
332 ContentSettingsPattern::FromURLNoWildcard(url
);
333 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
335 map
->SetWebsiteSetting(pattern
,
337 CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
,
342 // TODO(jww): This will revoke all of the decisions in the browser context.
343 // However, the networking stack actually keeps track of its own list of
344 // exceptions per-HttpNetworkTransaction in the SSLConfig structure (see the
345 // allowed_bad_certs Vector in net/ssl/ssl_config.h). This dual-tracking of
346 // exceptions introduces a problem where the browser context can revoke a
347 // certificate, but if a transaction reuses a cached version of the SSLConfig
348 // (probably from a pooled socket), it may bypass the intestitial layer.
350 // Over time, the cached versions should expire and it should converge on
351 // showing the interstitial. We probably need to introduce into the networking
352 // stack a way revoke SSLConfig's allowed_bad_certs lists per socket.
354 // For now, RevokeUserAllowExceptionsHard is our solution for the rare case
355 // where it is necessary to revoke the preferences immediately. It does so by
356 // flushing idle sockets, thus it is a big hammer and should be wielded with
357 // extreme caution as it can have a big, negative impact on network performance.
358 void ChromeSSLHostStateDelegate::RevokeUserAllowExceptionsHard(
359 const std::string
& host
) {
360 RevokeUserAllowExceptions(host
);
361 scoped_refptr
<net::URLRequestContextGetter
> getter(
362 profile_
->GetRequestContext());
363 getter
->GetNetworkTaskRunner()->PostTask(
364 FROM_HERE
, base::Bind(&CloseIdleConnections
, getter
));
367 bool ChromeSSLHostStateDelegate::HasAllowException(
368 const std::string
& host
) const {
369 GURL url
= GetSecureGURLForHost(host
);
370 const ContentSettingsPattern pattern
=
371 ContentSettingsPattern::FromURLNoWildcard(url
);
372 HostContentSettingsMap
* map
= profile_
->GetHostContentSettingsMap();
374 scoped_ptr
<base::Value
> value(map
->GetWebsiteSetting(
375 url
, url
, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS
, std::string(), NULL
));
377 if (!value
.get() || !value
->IsType(base::Value::TYPE_DICTIONARY
))
380 base::DictionaryValue
* dict
; // Owned by value
381 bool success
= value
->GetAsDictionary(&dict
);
384 for (base::DictionaryValue::Iterator
it(*dict
); !it
.IsAtEnd(); it
.Advance()) {
385 int policy_decision
; // Owned by dict
386 success
= it
.value().GetAsInteger(&policy_decision
);
387 if (success
&& (static_cast<CertJudgment
>(policy_decision
) == ALLOWED
))
394 void ChromeSSLHostStateDelegate::HostRanInsecureContent(const std::string
& host
,
396 ran_insecure_content_hosts_
.insert(BrokenHostEntry(host
, pid
));
399 bool ChromeSSLHostStateDelegate::DidHostRunInsecureContent(
400 const std::string
& host
,
402 return !!ran_insecure_content_hosts_
.count(BrokenHostEntry(host
, pid
));
404 void ChromeSSLHostStateDelegate::SetClock(scoped_ptr
<base::Clock
> clock
) {
405 clock_
.reset(clock
.release());