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