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

Unified 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, 5 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 side-by-side diff with in-line comments
Download patch
Index: chrome/browser/ssl/chrome_ssl_host_state_decisions.cc
diff --git a/chrome/browser/ssl/chrome_ssl_host_state_decisions.cc b/chrome/browser/ssl/chrome_ssl_host_state_decisions.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c8ee106eae3c6e8746dc498f71c64783ad3a836d
--- /dev/null
+++ b/chrome/browser/ssl/chrome_ssl_host_state_decisions.cc
@@ -0,0 +1,366 @@
+// Copyright (c) 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ssl/chrome_ssl_host_state_decisions.h"
+
+#include "base/base64.h"
+#include "base/command_line.h"
+#include "base/logging.h"
+#include "base/metrics/field_trial.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/time/default_clock.h"
+#include "base/time/time.h"
+#include "chrome/browser/content_settings/host_content_settings_map.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/common/chrome_switches.h"
+#include "components/content_settings/core/common/content_settings_types.h"
+#include "components/variations/variations_associated_data.h"
+#include "net/base/hash_value.h"
+#include "net/cert/x509_certificate.h"
+#include "url/gurl.h"
+
+namespace {
+
+// Switch value that specifies that certificate decisions should be forgotten at
+// the end of the current session.
+const int64 kForgetAtSessionEndSwitchValue = -1;
+
+// Experiment information
+const char kRememberCertificateErrorDecisionsFieldTrialName[] =
+ "RememberCertificateErrorDecisions";
+const char kRememberCertificateErrorDecisionsFieldTrialDefaultGroup[] =
+ "Default";
+const char kRememberCertificateErrorDecisionsFieldTrialLengthParam[] = "length";
+
+// Keys for the per-site error + certificate finger to judgement content
+// settings map.
+const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map";
+const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time";
+const char kSSLCertDecisionVersionKey[] = "version";
+
+const int kDefaultSSLCertDecisionVersion = 1;
+
+// This is a helper function that returns the length of time before a
+// certificate decision expires based on the command line flags. Returns a
+// non-negative value in seconds or a value of -1 indicating that decisions
+// should not be remembered after the current session has ended (but should be
+// remembered indefinitely as long as the session does not end), which is the
+// "old" style of certificate decision memory. Uses the experimental group
+// unless overridden by a command line flag.
+int64 GetExpirationDelta() {
+ // Check command line flags first to give them priority, then check
+ // experimental groups.
+ if (CommandLine::ForCurrentProcess()->HasSwitch(
+ switches::kRememberCertErrorDecisions)) {
+ std::string switch_value =
+ CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
+ switches::kRememberCertErrorDecisions);
+ int64 expiration_delta;
+ if (!base::StringToInt64(base::StringPiece(switch_value),
+ &expiration_delta) ||
+ expiration_delta < kForgetAtSessionEndSwitchValue) {
+ LOG(ERROR) << "Failed to parse the certificate error decision "
+ << "memory length: " << switch_value;
+ return kForgetAtSessionEndSwitchValue;
+ }
+
+ return expiration_delta;
+ }
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
+
+ // If the user is in the field trial, set the expiration to the length
+ // associated with that experimental group. The default group cannot have
+ // parameters associated with it, so it needs to be handled explictly.
+ std::string group_name = base::FieldTrialList::FindFullName(
+ kRememberCertificateErrorDecisionsFieldTrialName);
+ if (!group_name.empty() &&
+ group_name.compare(
+ kRememberCertificateErrorDecisionsFieldTrialDefaultGroup) != 0) {
+ int64 field_trial_param_length;
+ std::string param = variations::GetVariationParamValue(
+ kRememberCertificateErrorDecisionsFieldTrialName,
+ kRememberCertificateErrorDecisionsFieldTrialLengthParam);
+ if (!param.empty() && base::StringToInt64(base::StringPiece(param),
+ &field_trial_param_length)) {
+ return field_trial_param_length;
+ }
+ }
+
+ return kForgetAtSessionEndSwitchValue;
+}
+
+std::string GetKey(net::X509Certificate* cert, net::CertStatus error) {
+ // Since a security decision will be made based on the fingerprint, Chrome
+ // should use the SHA-256 fingerprint for the certificate.
+ net::SHA256HashValue fingerprint =
+ net::X509Certificate::CalculateChainFingerprint256(
+ cert->os_cert_handle(), cert->GetIntermediateCertificates());
+ std::string base64_fingerprint;
+ base::Base64Encode(
+ base::StringPiece(reinterpret_cast<const char*>(fingerprint.data),
+ sizeof(fingerprint.data)),
+ &base64_fingerprint);
+ return base::UintToString(error) + base64_fingerprint;
+}
+
+} // namespace
+
+// This helper function gets the dictionary of certificate fingerprints to
+// errors of certificates that have been accepted by the user from the content
+// dictionary that has been passed in. The returned pointer is owned by the the
+// argument dict that is passed in.
+//
+// If create_entries is set to |DoNotCreateDictionaryEntries|,
+// GetValidCertDecisionsDict will return NULL if there is anything invalid about
+// the setting, such as an invalid version or invalid value types (in addition
+// to there not be any values in the dictionary). If create_entries is set to
+// |CreateDictionaryEntries|, if no dictionary is found or the decisions are
+// expired, a new dictionary will be created
+base::DictionaryValue* ChromeSSLHostStateDecisions::GetValidCertDecisionsDict(
+ base::DictionaryValue* dict,
+ CreateDictionaryEntriesDisposition create_entries) {
+ // Extract the version of the certificate decision structure from the content
+ // setting.
+ int version;
+ bool success = dict->GetInteger(kSSLCertDecisionVersionKey, &version);
+ if (!success) {
+ if (create_entries == DoNotCreateDictionaryEntries)
+ return NULL;
+
+ dict->SetInteger(kSSLCertDecisionVersionKey,
+ kDefaultSSLCertDecisionVersion);
+ version = kDefaultSSLCertDecisionVersion;
+ }
+
+ // If the version is somehow a newer version than Chrome can handle, there's
+ // really nothing to do other than fail silently and pretend it doesn't exist
+ // (or is malformed).
+ if (version > kDefaultSSLCertDecisionVersion) {
+ LOG(ERROR) << "Failed to parse a certificate error exception that is in a "
+ << "newer version format (" << version << ") than is supported ("
+ << kDefaultSSLCertDecisionVersion << ")";
+ return NULL;
+ }
+
+ // Extract the certificate decision's expiration time from the content
+ // setting. If there is no expiration time, that means it should never expire
+ // and it should reset only at session restart, so skip all of the expiration
+ // checks.
+ bool expired = false;
+ base::Time now = clock_->Now();
+ base::Time decision_expiration;
+ if (dict->HasKey(kSSLCertDecisionExpirationTimeKey)) {
+ std::string decision_expiration_string;
+ int64 decision_expiration_int64;
+ success = dict->GetString(kSSLCertDecisionExpirationTimeKey,
+ &decision_expiration_string);
+ if (!base::StringToInt64(base::StringPiece(decision_expiration_string),
+ &decision_expiration_int64)) {
+ LOG(ERROR) << "Failed to parse a certificate error exception that has a "
+ << "bad value for an expiration time: "
+ << decision_expiration_string;
+ return NULL;
+ }
+ decision_expiration =
+ base::Time::FromInternalValue(decision_expiration_int64);
+ }
+
+ // Check to see if the user's certificate decision has expired.
+ // - Expired and |create_entries| is DoNotCreateDictionaryEntries, return
+ // NULL.
+ // - Expired and |create_entries| is CreateDictionaryEntries, update the
+ // expiration time.
+ if (should_remember_ssl_decisions_ !=
+ ForgetSSLExceptionDecisionsAtSessionEnd &&
+ decision_expiration.ToInternalValue() <= now.ToInternalValue()) {
+ if (create_entries == DoNotCreateDictionaryEntries)
+ return NULL;
+
+ expired = true;
+
+ base::Time expiration_time =
+ now + default_ssl_cert_decision_expiration_delta_;
+ // Unfortunately, JSON (and thus content settings) doesn't support int64
+ // values, only doubles. Since this mildly depends on precision, it is
+ // better to store the value as a string.
+ dict->SetString(kSSLCertDecisionExpirationTimeKey,
+ base::Int64ToString(expiration_time.ToInternalValue()));
+ }
+
+ // Extract the map of certificate fingerprints to errors from the setting.
+ base::DictionaryValue* cert_error_dict; // Will be owned by dict
+ if (expired ||
+ !dict->GetDictionary(kSSLCertDecisionCertErrorMapKey, &cert_error_dict)) {
+ if (create_entries == DoNotCreateDictionaryEntries)
+ return NULL;
+
+ cert_error_dict = new base::DictionaryValue();
+ // dict takes ownership of cert_error_dict
+ dict->Set(kSSLCertDecisionCertErrorMapKey, cert_error_dict);
+ }
+
+ return cert_error_dict;
+}
+
+// If |should_remember_ssl_decisions_| is
+// ForgetSSLExceptionDecisionsAtSessionEnd, that means that all invalid
+// certificate proceed decisions should be forgotten when the session ends. At
+// attempt is made in the destructor to remove the entries, but in the case that
+// things didn't shut down cleanly, on start, Clear is called to guarantee a
+// clean state.
+ChromeSSLHostStateDecisions::ChromeSSLHostStateDecisions(Profile* profile)
+ : clock_(new base::DefaultClock()), profile_(profile) {
+ int64 expiration_delta = GetExpirationDelta();
+ if (expiration_delta == kForgetAtSessionEndSwitchValue) {
+ should_remember_ssl_decisions_ = ForgetSSLExceptionDecisionsAtSessionEnd;
+ expiration_delta = 0;
+ Clear();
+ } else {
+ should_remember_ssl_decisions_ = RememberSSLExceptionDecisionsForDelta;
+ }
+ default_ssl_cert_decision_expiration_delta_ =
+ base::TimeDelta::FromSeconds(expiration_delta);
+}
+
+ChromeSSLHostStateDecisions::~ChromeSSLHostStateDecisions() {
+ if (should_remember_ssl_decisions_ == ForgetSSLExceptionDecisionsAtSessionEnd)
+ Clear();
+}
+
+void ChromeSSLHostStateDecisions::DenyCert(const GURL& url,
+ net::X509Certificate* cert,
+ net::CertStatus error) {
+ ChangeCertPolicy(url, cert, error, net::CertPolicy::DENIED);
+}
+
+void ChromeSSLHostStateDecisions::AllowCert(const GURL& url,
+ net::X509Certificate* cert,
+ net::CertStatus error) {
+ ChangeCertPolicy(url, cert, error, net::CertPolicy::ALLOWED);
+}
+
+void ChromeSSLHostStateDecisions::Clear() {
+ profile_->GetHostContentSettingsMap()->ClearSettingsForOneType(
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS);
+}
+
+net::CertPolicy::Judgment ChromeSSLHostStateDecisions::QueryPolicy(
+ const GURL& url,
+ net::X509Certificate* cert,
+ net::CertStatus error) {
+ HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
+ scoped_ptr<base::Value> value(
+ map->GetWebsiteSetting(url.GetWithEmptyPath(),
+ url.GetWithEmptyPath(),
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
+ std::string(),
+ NULL));
+
+ if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
+ return net::CertPolicy::UNKNOWN;
+
+ base::DictionaryValue* dict; // Owned by value
+ int policy_decision;
+ bool success = value->GetAsDictionary(&dict);
+ DCHECK(success);
+
+ base::DictionaryValue* cert_error_dict; // Owned by value
+ cert_error_dict =
+ GetValidCertDecisionsDict(dict, DoNotCreateDictionaryEntries);
+ if (!cert_error_dict)
+ return net::CertPolicy::UNKNOWN;
+
+ success = cert_error_dict->GetIntegerWithoutPathExpansion(GetKey(cert, error),
+ &policy_decision);
+
+ if (!success)
+ return net::CertPolicy::Judgment::UNKNOWN;
+
+ 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.
+}
+
+void ChromeSSLHostStateDecisions::RevokeAllowAndDenyPreferences(
+ const GURL& url) {
+ const ContentSettingsPattern pattern =
+ ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
+ HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
+
+ map->SetWebsiteSetting(pattern,
+ pattern,
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
+ std::string(),
+ NULL);
+}
+
+bool ChromeSSLHostStateDecisions::HasAllowedOrDeniedCert(const GURL& url) {
+ const ContentSettingsPattern pattern =
+ ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
+ HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
+
+ scoped_ptr<base::Value> value(
+ map->GetWebsiteSetting(url.GetWithEmptyPath(),
+ url.GetWithEmptyPath(),
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
+ std::string(),
+ NULL));
+
+ if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
+ return false;
+
+ base::DictionaryValue* dict; // Owned by value
+ bool success = value->GetAsDictionary(&dict);
+ DCHECK(success);
+
+ for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); it.Advance()) {
+ int policy_decision; // Owned by dict
+ success = it.value().GetAsInteger(&policy_decision);
+ if (success && (static_cast<net::CertPolicy::Judgment>(policy_decision) !=
+ net::CertPolicy::UNKNOWN))
+ return true;
+ }
+
+ return false;
+}
+
+void ChromeSSLHostStateDecisions::ChangeCertPolicy(
+ const GURL& url,
+ net::X509Certificate* cert,
+ net::CertStatus error,
+ net::CertPolicy::Judgment judgment) {
+ const ContentSettingsPattern pattern =
+ ContentSettingsPattern::FromURLNoWildcard(url.GetWithEmptyPath());
+ HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
+ scoped_ptr<base::Value> value(
+ map->GetWebsiteSetting(url.GetWithEmptyPath(),
+ url.GetWithEmptyPath(),
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
+ std::string(),
+ NULL));
+
+ if (!value.get() || !value->IsType(base::Value::TYPE_DICTIONARY))
+ value.reset(new base::DictionaryValue());
+
+ base::DictionaryValue* dict;
+ bool success = value->GetAsDictionary(&dict);
+ DCHECK(success);
+
+ base::DictionaryValue* cert_dict =
+ GetValidCertDecisionsDict(dict, CreateDictionaryEntries);
+ // If a a valid certificate dictionary cannot be extracted from the content
+ // setting, that means it's in an unknown format. Unfortunately, there's
+ // nothing to be done in that case, so a silent fail is the only option.
+ if (!cert_dict)
+ return;
+
+ dict->SetIntegerWithoutPathExpansion(kSSLCertDecisionVersionKey,
+ kDefaultSSLCertDecisionVersion);
+ cert_dict->SetIntegerWithoutPathExpansion(GetKey(cert, error), judgment);
+
+ // The map takes ownership of the value, so it is released in the call to
+ // SetWebsiteSetting.
+ map->SetWebsiteSetting(pattern,
+ pattern,
+ CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
+ std::string(),
+ value.release());
+}

Powered by Google App Engine
This is Rietveld 408576698