blob: c306b22f0a9295b7f8e582a8e977fa859be4bc42 [file] [log] [blame]
// Copyright 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_delegate.h"
#include "base/base64.h"
#include "base/bind.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/clock.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 "net/http/http_transaction_factory.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.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;
// Closes all idle network connections for the given URLRequestContext. This is
// a big hammer and should be wielded with extreme caution as it can have a big,
// negative impact on network performance. In this case, it is used by
// RevokeUserDecisionsHard, which should only be called by rare, user initiated
// events. See the comment before RevokeUserDecisionsHard implementation for
// more information.
void CloseIdleConnections(
scoped_refptr<net::URLRequestContextGetter> url_request_context_getter) {
url_request_context_getter->
GetURLRequestContext()->
http_transaction_factory()->
GetSession()->
CloseIdleConnections();
}
// All SSL decisions are per host (and are shared arcoss schemes), so this
// canonicalizes all hosts into a secure scheme GURL to use with content
// settings. The returned GURL will be the passed in host with an empty path and
// https:// as the scheme.
GURL GetSecureGURLForHost(const std::string& host) {
std::string url = "https://" + host;
return GURL(url);
}
// 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;
}
// 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* ChromeSSLHostStateDelegate::GetValidCertDecisionsDict(
base::DictionaryValue* dict,
CreateDictionaryEntriesDisposition create_entries,
bool* expired_previous_decision) {
// This needs to be done first in case the method is short circuited by an
// early failure.
*expired_previous_decision = false;
// 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()) {
*expired_previous_decision = true;
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 = NULL; // 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.
ChromeSSLHostStateDelegate::ChromeSSLHostStateDelegate(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);
}
ChromeSSLHostStateDelegate::~ChromeSSLHostStateDelegate() {
if (should_remember_ssl_decisions_ == ForgetSSLExceptionDecisionsAtSessionEnd)
Clear();
}
void ChromeSSLHostStateDelegate::DenyCert(const std::string& host,
net::X509Certificate* cert,
net::CertStatus error) {
ChangeCertPolicy(host, cert, error, net::CertPolicy::DENIED);
}
void ChromeSSLHostStateDelegate::AllowCert(const std::string& host,
net::X509Certificate* cert,
net::CertStatus error) {
ChangeCertPolicy(host, cert, error, net::CertPolicy::ALLOWED);
}
void ChromeSSLHostStateDelegate::Clear() {
profile_->GetHostContentSettingsMap()->ClearSettingsForOneType(
CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS);
}
net::CertPolicy::Judgment ChromeSSLHostStateDelegate::QueryPolicy(
const std::string& host,
net::X509Certificate* cert,
net::CertStatus error,
bool* expired_previous_decision) {
HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
GURL url = GetSecureGURLForHost(host);
scoped_ptr<base::Value> value(map->GetWebsiteSetting(
url, url, CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS, std::string(), NULL));
// Set a default value in case this method is short circuited and doesn't do a
// full query.
*expired_previous_decision = false;
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, expired_previous_decision);
if (!cert_error_dict) {
// This revoke is necessary to clear any old expired setting that may
// lingering in the case that an old decision expried.
RevokeUserDecisions(host);
return net::CertPolicy::UNKNOWN;
}
success = cert_error_dict->GetIntegerWithoutPathExpansion(GetKey(cert, error),
&policy_decision);
// If a policy decision was successfully retrieved and it's a valid value of
// ALLOWED or DENIED, return the valid value. Otherwise, return UNKNOWN.
if (success && policy_decision == net::CertPolicy::Judgment::ALLOWED)
return net::CertPolicy::Judgment::ALLOWED;
else if (success && policy_decision == net::CertPolicy::Judgment::DENIED)
return net::CertPolicy::Judgment::DENIED;
return net::CertPolicy::Judgment::UNKNOWN;
}
void ChromeSSLHostStateDelegate::RevokeUserDecisions(const std::string& host) {
GURL url = GetSecureGURLForHost(host);
const ContentSettingsPattern pattern =
ContentSettingsPattern::FromURLNoWildcard(url);
HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
map->SetWebsiteSetting(pattern,
pattern,
CONTENT_SETTINGS_TYPE_SSL_CERT_DECISIONS,
std::string(),
NULL);
}
// TODO(jww): This will revoke all of the decisions in the browser context.
// However, the networking stack actually keeps track of its own list of
// exceptions per-HttpNetworkTransaction in the SSLConfig structure (see the
// allowed_bad_certs Vector in net/ssl/ssl_config.h). This dual-tracking of
// exceptions introduces a problem where the browser context can revoke a
// certificate, but if a transaction reuses a cached version of the SSLConfig
// (probably from a pooled socket), it may bypass the intestitial layer.
//
// Over time, the cached versions should expire and it should converge on
// showing the interstitial. We probably need to introduce into the networking
// stack a way revoke SSLConfig's allowed_bad_certs lists per socket.
//
// For now, RevokeUserDecisionsHard is our solution for the rare case where it
// is necessary to revoke the preferences immediately. It does so by flushing
// idle sockets.
void ChromeSSLHostStateDelegate::RevokeUserDecisionsHard(
const std::string& host) {
RevokeUserDecisions(host);
scoped_refptr<net::URLRequestContextGetter> getter(
profile_->GetRequestContext());
profile_->GetRequestContext()->GetNetworkTaskRunner()->PostTask(
FROM_HERE, base::Bind(&CloseIdleConnections, getter));
}
bool ChromeSSLHostStateDelegate::HasUserDecision(const std::string& host) {
GURL url = GetSecureGURLForHost(host);
const ContentSettingsPattern pattern =
ContentSettingsPattern::FromURLNoWildcard(url);
HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
scoped_ptr<base::Value> value(map->GetWebsiteSetting(
url, url, 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 ChromeSSLHostStateDelegate::HostRanInsecureContent(const std::string& host,
int pid) {
ran_insecure_content_hosts_.insert(BrokenHostEntry(host, pid));
}
bool ChromeSSLHostStateDelegate::DidHostRunInsecureContent(
const std::string& host,
int pid) const {
return !!ran_insecure_content_hosts_.count(BrokenHostEntry(host, pid));
}
void ChromeSSLHostStateDelegate::SetClock(scoped_ptr<base::Clock> clock) {
clock_.reset(clock.release());
}
void ChromeSSLHostStateDelegate::ChangeCertPolicy(
const std::string& host,
net::X509Certificate* cert,
net::CertStatus error,
net::CertPolicy::Judgment judgment) {
GURL url = GetSecureGURLForHost(host);
const ContentSettingsPattern pattern =
ContentSettingsPattern::FromURLNoWildcard(url);
HostContentSettingsMap* map = profile_->GetHostContentSettingsMap();
scoped_ptr<base::Value> value(map->GetWebsiteSetting(
url, url, 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);
bool expired_previous_decision; // unused value in this function
base::DictionaryValue* cert_dict = GetValidCertDecisionsDict(
dict, CreateDictionaryEntries, &expired_previous_decision);
// 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());
}