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.
4
5#include "chrome/browser/ssl/chrome_ssl_host_state_delegate.h"
6
7#include <set>
8
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/content_settings/host_content_settings_map.h"
21#include "chrome/browser/profiles/profile.h"
22#include "chrome/common/chrome_switches.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/cert/x509_certificate.h"
27#include "net/http/http_transaction_factory.h"
28#include "net/url_request/url_request_context.h"
29#include "net/url_request/url_request_context_getter.h"
30#include "url/gurl.h"
31
32namespace {
33
34// Switch value that specifies that certificate decisions should be forgotten at
35// the end of the current session.
36const int64 kForgetAtSessionEndSwitchValue = -1;
37
38// Experiment information
39const char kRememberCertificateErrorDecisionsFieldTrialName[] =
40    "RememberCertificateErrorDecisions";
41const char kRememberCertificateErrorDecisionsFieldTrialDefaultGroup[] =
42    "Default";
43const char kRememberCertificateErrorDecisionsFieldTrialLengthParam[] = "length";
44
45// Keys for the per-site error + certificate finger to judgment content
46// settings map.
47const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map";
48const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time";
49const char kSSLCertDecisionVersionKey[] = "version";
50const char kSSLCertDecisionGUIDKey[] = "guid";
51
52const int kDefaultSSLCertDecisionVersion = 1;
53
54void CloseIdleConnections(
55    scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) {
56  url_request_context_getter->
57      GetURLRequestContext()->
58      http_transaction_factory()->
59      GetSession()->
60      CloseIdleConnections();
61}
62
63// All SSL decisions are per host (and are shared arcoss schemes), so this
64// canonicalizes all hosts into a secure scheme GURL to use with content
65// settings. The returned GURL will be the passed in host with an empty path and
66// https:// as the scheme.
67GURL GetSecureGURLForHost(const std::string& host) {
68  std::string url = "https://" + host;
69  return GURL(url);
70}
71
72// This is a helper function that returns the length of time before a
73// certificate decision expires based on the command line flags. Returns a
74// non-negative value in seconds or a value of -1 indicating that decisions
75// should not be remembered after the current session has ended (but should be
76// remembered indefinitely as long as the session does not end), which is the
77// "old" style of certificate decision memory. Uses the experimental group
78// unless overridden by a command line flag.
79int64 GetExpirationDelta() {
80  // Check command line flags first to give them priority, then check
81  // experimental groups.
82  if (CommandLine::ForCurrentProcess()->HasSwitch(
83          switches::kRememberCertErrorDecisions)) {
84    std::string switch_value =
85        CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
86            switches::kRememberCertErrorDecisions);
87    int64 expiration_delta;
88    if (!base::StringToInt64(base::StringPiece(switch_value),
89                             &expiration_delta) ||
90        expiration_delta < kForgetAtSessionEndSwitchValue) {
91      LOG(ERROR) << "Failed to parse the certificate error decision "
92                 << "memory length: " << switch_value;
93      return kForgetAtSessionEndSwitchValue;
94    }
95
96    return expiration_delta;
97  }
98
99  // If the user is in the field trial, set the expiration to the length
100  // associated with that experimental group.  The default group cannot have
101  // parameters associated with it, so it needs to be handled explictly.
102  std::string group_name = base::FieldTrialList::FindFullName(
103      kRememberCertificateErrorDecisionsFieldTrialName);
104  if (!group_name.empty() &&
105      group_name.compare(
106          kRememberCertificateErrorDecisionsFieldTrialDefaultGroup) != 0) {
107    int64 field_trial_param_length;
108    std::string param = variations::GetVariationParamValue(
109        kRememberCertificateErrorDecisionsFieldTrialName,
110        kRememberCertificateErrorDecisionsFieldTrialLengthParam);
111    if (!param.empty() && base::StringToInt64(base::StringPiece(param),
112                                              &field_trial_param_length)) {
113      return field_trial_param_length;
114    }
115  }
116
117  return kForgetAtSessionEndSwitchValue;
118}
119
120std::string GetKey(const net::X509Certificate& cert, net::CertStatus error) {
121  // Since a security decision will be made based on the fingerprint, Chrome
122  // should use the SHA-256 fingerprint for the certificate.
123  net::SHA256HashValue fingerprint =
124      net::X509Certificate::CalculateChainFingerprint256(
125          cert.os_cert_handle(), cert.GetIntermediateCertificates());
126  std::string base64_fingerprint;
127  base::Base64Encode(
128      base::StringPiece(reinterpret_cast<const char*>(fingerprint.data),
129                        sizeof(fingerprint.data)),
130      &base64_fingerprint);
131  return base::UintToString(error) + base64_fingerprint;
132}
133
134}  // namespace
135
136// This helper function gets the dictionary of certificate fingerprints to
137// errors of certificates that have been accepted by the user from the content
138// dictionary that has been passed in. The returned pointer is owned by the the
139// argument dict that is passed in.
140//
141// If create_entries is set to |DO_NOT_CREATE_DICTIONARY_ENTRIES|,
142// GetValidCertDecisionsDict will return NULL if there is anything invalid about
143// the setting, such as an invalid version or invalid value types (in addition
144// to there not being any values in the dictionary). If create_entries is set to
145// |CREATE_DICTIONARY_ENTRIES|, if no dictionary is found or the decisions are
146// expired, a new dictionary will be created.
147base::DictionaryValue* ChromeSSLHostStateDelegate::GetValidCertDecisionsDict(
148    base::DictionaryValue* dict,
149    CreateDictionaryEntriesDisposition create_entries,
150    bool* expired_previous_decision) {
151  // This needs to be done first in case the method is short circuited by an
152  // early failure.
153  *expired_previous_decision = false;
154
155  // Extract the version of the certificate decision structure from the content
156  // setting.
157  int version;
158  bool success = dict->GetInteger(kSSLCertDecisionVersionKey, &version);
159  if (!success) {
160    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
161      return NULL;
162
163    dict->SetInteger(kSSLCertDecisionVersionKey,
164                     kDefaultSSLCertDecisionVersion);
165    version = kDefaultSSLCertDecisionVersion;
166  }
167
168  // If the version is somehow a newer version than Chrome can handle, there's
169  // really nothing to do other than fail silently and pretend it doesn't exist
170  // (or is malformed).
171  if (version > kDefaultSSLCertDecisionVersion) {
172    LOG(ERROR) << "Failed to parse a certificate error exception that is in a "
173               << "newer version format (" << version << ") than is supported ("
174               << kDefaultSSLCertDecisionVersion << ")";
175    return NULL;
176  }
177
178  // Extract the certificate decision's expiration time from the content
179  // setting. If there is no expiration time, that means it should never expire
180  // and it should reset only at session restart, so skip all of the expiration
181  // checks.
182  bool expired = false;
183  base::Time now = clock_->Now();
184  base::Time decision_expiration;
185  if (dict->HasKey(kSSLCertDecisionExpirationTimeKey)) {
186    std::string decision_expiration_string;
187    int64 decision_expiration_int64;
188    success = dict->GetString(kSSLCertDecisionExpirationTimeKey,
189                              &decision_expiration_string);
190    if (!base::StringToInt64(base::StringPiece(decision_expiration_string),
191                             &decision_expiration_int64)) {
192      LOG(ERROR) << "Failed to parse a certificate error exception that has a "
193                 << "bad value for an expiration time: "
194                 << decision_expiration_string;
195      return NULL;
196    }
197    decision_expiration =
198        base::Time::FromInternalValue(decision_expiration_int64);
199  }
200
201  // Check to see if the user's certificate decision has expired.
202  // - Expired and |create_entries| is DO_NOT_CREATE_DICTIONARY_ENTRIES, return
203  // NULL.
204  // - Expired and |create_entries| is CREATE_DICTIONARY_ENTRIES, update the
205  // expiration time.
206  if (should_remember_ssl_decisions_ !=
207          FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END &&
208      decision_expiration.ToInternalValue() <= now.ToInternalValue()) {
209    *expired_previous_decision = true;
210
211    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
212      return NULL;
213
214    expired = true;
215    base::Time expiration_time =
216        now + default_ssl_cert_decision_expiration_delta_;
217    // Unfortunately, JSON (and thus content settings) doesn't support int64
218    // values, only doubles. Since this mildly depends on precision, it is
219    // better to store the value as a string.
220    dict->SetString(kSSLCertDecisionExpirationTimeKey,
221                    base::Int64ToString(expiration_time.ToInternalValue()));
222  } else if (should_remember_ssl_decisions_ ==
223             FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END) {
224    if (dict->HasKey(kSSLCertDecisionGUIDKey)) {
225      std::string old_expiration_guid;
226      success = dict->GetString(kSSLCertDecisionGUIDKey, &old_expiration_guid);
227      if (old_expiration_guid.compare(current_expiration_guid_) != 0) {
228        *expired_previous_decision = true;
229        expired = true;
230      }
231    }
232  }
233
234  dict->SetString(kSSLCertDecisionGUIDKey, current_expiration_guid_);
235
236  // Extract the map of certificate fingerprints to errors from the setting.
237  base::DictionaryValue* cert_error_dict = NULL;  // Will be owned by dict
238  if (expired ||
239      !dict->GetDictionary(kSSLCertDecisionCertErrorMapKey, &cert_error_dict)) {
240    if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
241      return NULL;
242
243    cert_error_dict = new base::DictionaryValue();
244    // dict takes ownership of cert_error_dict
245    dict->Set(kSSLCertDecisionCertErrorMapKey, cert_error_dict);
246  }
247
248  return cert_error_dict;
249}
250
251// If |should_remember_ssl_decisions_| is
252// FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END, that means that all invalid
253// certificate proceed decisions should be forgotten when the session ends. To
254// simulate that, Chrome keeps track of a guid to represent the current browser
255// session and stores it in decision entries. See the comment for
256// |current_expiration_guid_| for more information.
257ChromeSSLHostStateDelegate::ChromeSSLHostStateDelegate(Profile* profile)
258    : clock_(new base::DefaultClock()),
259      profile_(profile),
260      current_expiration_guid_(base::GenerateGUID()) {
261  int64 expiration_delta = GetExpirationDelta();
262  if (expiration_delta == kForgetAtSessionEndSwitchValue) {
263    should_remember_ssl_decisions_ =
264        FORGET_SSL_EXCEPTION_DECISIONS_AT_SESSION_END;
265    expiration_delta = 0;
266  } else {
267    should_remember_ssl_decisions_ = REMEMBER_SSL_EXCEPTION_DECISIONS_FOR_DELTA;
268  }
269  default_ssl_cert_decision_expiration_delta_ =
270      base::TimeDelta::FromSeconds(expiration_delta);
271}
272
273ChromeSSLHostStateDelegate::~ChromeSSLHostStateDelegate() {
274}
275
276void ChromeSSLHostStateDelegate::AllowCert(const std::string& host,
277                                           const net::X509Certificate& cert,
278                                           net::CertStatus error) {
279  GURL url = GetSecureGURLForHost(host);
280  const ContentSettingsPattern pattern =
281      ContentSettingsPattern::FromURLNoWildcard(url);
282  HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
283  scoped_ptr<base::Value> value(map->GetWebsiteSetting(
284      url, url, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, std::string(), NULL));
285
286  if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
287    value.reset(new base::DictionaryValue());
288
289  base::DictionaryValue* dict;
290  bool success = value->GetAsDictionary(&dict);
291  DCHECK(success);
292
293  bool expired_previous_decision;  // unused value in this function
294  base::DictionaryValue* cert_dict = GetValidCertDecisionsDict(
295      dict, CREATE_DICTIONARY_ENTRIES, &expired_previous_decision);
296  // If a a valid certificate dictionary cannot be extracted from the content
297  // setting, that means it's in an unknown format. Unfortunately, there's
298  // nothing to be done in that case, so a silent fail is the only option.
299  if (!cert_dict)
300    return;
301
302  dict->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey,
303                                       kDefaultSSLCertDecisionVersion);
304  cert_dict->SetIntegerWithoutPathExpansion(GetKey(cert, error), ALLOWED);
305
306  // The map takes ownership of the value, so it is released in the call to
307  // SetWebsiteSetting.
308  map->SetWebsiteSetting(pattern,
309                         pattern,
310                         CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
311                         std::string(),
312                         value.release());
313}
314
315void ChromeSSLHostStateDelegate::Clear() {
316  profile_->GetHostContentSettingsMap()->ClearSettingsForOneType(
317      CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS);
318}
319
320content::SSLHostStateDelegate::CertJudgment
321ChromeSSLHostStateDelegate::QueryPolicy(const std::string& host,
322                                        const net::X509Certificate& cert,
323                                        net::CertStatus error,
324                                        bool* expired_previous_decision) {
325  HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
326  GURL url = GetSecureGURLForHost(host);
327  scoped_ptr<base::Value> value(map->GetWebsiteSetting(
328      url, url, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, std::string(), NULL));
329
330  // Set a default value in case this method is short circuited and doesn't do a
331  // full query.
332  *expired_previous_decision = false;
333  if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
334    return DENIED;
335
336  base::DictionaryValue* dict;  // Owned by value
337  int policy_decision;
338  bool success = value->GetAsDictionary(&dict);
339  DCHECK(success);
340
341  base::DictionaryValue* cert_error_dict;  // Owned by value
342  cert_error_dict = GetValidCertDecisionsDict(
343      dict, DO_NOT_CREATE_DICTIONARY_ENTRIES, expired_previous_decision);
344  if (!cert_error_dict) {
345    // This revoke is necessary to clear any old expired setting that may be
346    // lingering in the case that an old decision expried.
347    RevokeUserAllowExceptions(host);
348    return DENIED;
349  }
350
351  success = cert_error_dict->GetIntegerWithoutPathExpansion(GetKey(cert, error),
352                                                            &policy_decision);
353
354  // If a policy decision was successfully retrieved and it's a valid value of
355  // ALLOWED, return the valid value. Otherwise, return DENIED.
356  if (success && policy_decision == ALLOWED)
357    return ALLOWED;
358
359  return DENIED;
360}
361
362void ChromeSSLHostStateDelegate::RevokeUserAllowExceptions(
363    const std::string& host) {
364  GURL url = GetSecureGURLForHost(host);
365  const ContentSettingsPattern pattern =
366      ContentSettingsPattern::FromURLNoWildcard(url);
367  HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
368
369  map->SetWebsiteSetting(pattern,
370                         pattern,
371                         CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
372                         std::string(),
373                         NULL);
374}
375
376// TODO(jww): This will revoke all of the decisions in the browser context.
377// However, the networking stack actually keeps track of its own list of
378// exceptions per-HttpNetworkTransaction in the SSLConfig structure (see the
379// allowed_bad_certs Vector in net/ssl/ssl_config.h). This dual-tracking of
380// exceptions introduces a problem where the browser context can revoke a
381// certificate, but if a transaction reuses a cached version of the SSLConfig
382// (probably from a pooled socket), it may bypass the intestitial layer.
383//
384// Over time, the cached versions should expire and it should converge on
385// showing the interstitial. We probably need to introduce into the networking
386// stack a way revoke SSLConfig's allowed_bad_certs lists per socket.
387//
388// For now, RevokeUserAllowExceptionsHard is our solution for the rare case
389// where it is necessary to revoke the preferences immediately. It does so by
390// flushing idle sockets, thus it is a big hammer and should be wielded with
391// extreme caution as it can have a big, negative impact on network performance.
392void ChromeSSLHostStateDelegate::RevokeUserAllowExceptionsHard(
393    const std::string& host) {
394  RevokeUserAllowExceptions(host);
395  scoped_refptr<net::URLRequestContextGetter> getter(
396      profile_->GetRequestContext());
397  getter->GetNetworkTaskRunner()->PostTask(
398      FROM_HERE, base::Bind(&CloseIdleConnections, getter));
399}
400
401bool ChromeSSLHostStateDelegate::HasAllowException(
402    const std::string& host) const {
403  GURL url = GetSecureGURLForHost(host);
404  const ContentSettingsPattern pattern =
405      ContentSettingsPattern::FromURLNoWildcard(url);
406  HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
407
408  scoped_ptr<base::Value> value(map->GetWebsiteSetting(
409      url, url, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, std::string(), NULL));
410
411  if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
412    return false;
413
414  base::DictionaryValue* dict;  // Owned by value
415  bool success = value->GetAsDictionary(&dict);
416  DCHECK(success);
417
418  for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) {
419    int policy_decision;  // Owned by dict
420    success = it.value().GetAsInteger(&policy_decision);
421    if (success && (static_cast<CertJudgment>(policy_decision) == ALLOWED))
422      return true;
423  }
424
425  return false;
426}
427
428void ChromeSSLHostStateDelegate::HostRanInsecureContent(const std::string& host,
429                                                        int pid) {
430  ran_insecure_content_hosts_.insert(BrokenHostEntry(host, pid));
431}
432
433bool ChromeSSLHostStateDelegate::DidHostRunInsecureContent(
434    const std::string& host,
435    int pid) const {
436  return !!ran_insecure_content_hosts_.count(BrokenHostEntry(host, pid));
437}
438void ChromeSSLHostStateDelegate::SetClock(scoped_ptr<base::Clock> clock) {
439  clock_.reset(clock.release());
440}
441