blob: 94670c93b186f9d0fe4c5c80c169e21236c0db5b [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 "components/security_interstitials/content/stateful_ssl_host_state_delegate.h"
#include <stdint.h>
#include <functional>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include "base/base64.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/field_trial_params.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 "base/values.h"
#include "build/build_config.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/security_interstitials/core/pref_names.h"
#include "components/variations/variations_associated_data.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "net/base/hash_value.h"
#include "net/base/url_util.h"
#include "net/cert/x509_certificate.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/mojom/network_context.mojom.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
namespace {
#if defined(OS_ANDROID)
StatefulSSLHostStateDelegate::RecurrentInterstitialMode
kRecurrentInterstitialDefaultMode =
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::PREF;
#else
StatefulSSLHostStateDelegate::RecurrentInterstitialMode
kRecurrentInterstitialDefaultMode =
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::IN_MEMORY;
#endif
// The number of times an error must recur before the recurrent error message is
// shown.
constexpr int kRecurrentInterstitialDefaultThreshold = 3;
// If "mode" is "pref", a pref stores the time at which each error most recently
// occurred, and the recurrent error message is shown if the error has recurred
// more than the threshold number of times with the most recent instance being
// less than |kRecurrentInterstitialResetTimeParam| seconds in the past. The
// default is 3 days.
constexpr int kRecurrentInterstitialDefaultResetTime =
259200; // 3 days in seconds
// The default expiration for certificate error bypasses is one week, unless
// overidden by a field trial group. See https://crbug.com/487270.
const uint64_t kDeltaDefaultExpirationInSeconds = UINT64_C(604800);
// Keys for the per-site error + certificate finger to judgment content
// settings map.
const char kSSLCertDecisionCertErrorMapKey[] = "cert_exceptions_map";
const char kSSLCertDecisionExpirationTimeKey[] = "decision_expiration_time";
const char kSSLCertDecisionVersionKey[] = "version";
const int kDefaultSSLCertDecisionVersion = 1;
// Records a new occurrence of |error|. The occurrence is stored in the
// recurrent interstitial pref, which keeps track of the most recent timestamps
// at which each error type occurred (up to the |threshold| most recent
// instances per error). The list is reset if the clock has gone backwards at
// any point.
void UpdateRecurrentInterstitialPref(PrefService* pref_service,
base::Clock* clock,
int error,
int threshold) {
double now = clock->Now().ToJsTime();
DictionaryPrefUpdate pref_update(pref_service,
prefs::kRecurrentSSLInterstitial);
base::Value* list_value =
pref_update->FindListKey(net::ErrorToShortString(error));
if (list_value) {
// Check that the values are in increasing order and wipe out the list if
// not (presumably because the clock changed).
double previous = 0;
for (const auto& error_instance : list_value->GetList()) {
double error_time = error_instance.GetDouble();
if (error_time < previous) {
list_value = nullptr;
break;
}
previous = error_time;
}
if (now < previous)
list_value = nullptr;
}
if (!list_value) {
// Either there was no list of occurrences of this error, or it was corrupt
// (i.e. out of order). Save a new list composed of just this one error
// instance.
base::Value error_list(base::Value::Type::LIST);
error_list.Append(now);
pref_update->SetKey(net::ErrorToShortString(error), std::move(error_list));
} else {
// Only up to |threshold| values need to be stored. If the list already
// contains |threshold| values, pop one off the front and append the new one
// at the end; otherwise just append the new one.
while (base::MakeStrictNum(list_value->GetList().size()) >= threshold) {
list_value->EraseListIter(list_value->GetList().begin());
}
list_value->Append(now);
}
}
bool DoesRecurrentInterstitialPrefMeetThreshold(PrefService* pref_service,
base::Clock* clock,
int error,
int threshold,
int error_reset_time) {
const base::Value* pref =
pref_service->GetDictionary(prefs::kRecurrentSSLInterstitial);
const base::Value* list_value =
pref->FindListKey(net::ErrorToShortString(error));
if (!list_value)
return false;
base::Time cutoff_time;
cutoff_time = clock->Now() - base::TimeDelta::FromSeconds(error_reset_time);
// Assume that the values in the list are in increasing order;
// UpdateRecurrentInterstitialPref() maintains this ordering. Check if there
// are more than |threshold| values after the cutoff time.
base::Value::ConstListView error_list = list_value->GetList();
for (size_t i = 0; i < error_list.size(); i++) {
if (base::Time::FromJsTime(error_list[i].GetDouble()) >= cutoff_time)
return base::MakeStrictNum(error_list.size() - i) >= threshold;
}
return false;
}
// 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);
}
std::string GetKey(const net::X509Certificate& cert, int 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 = cert.CalculateChainFingerprint256();
std::string base64_fingerprint;
base::Base64Encode(
base::StringPiece(reinterpret_cast<const char*>(fingerprint.data),
sizeof(fingerprint.data)),
&base64_fingerprint);
return base::NumberToString(error) + base64_fingerprint;
}
bool HostFilterToPatternFilter(
base::OnceCallback<bool(const std::string&)> host_filter,
const ContentSettingsPattern& primary_pattern,
const ContentSettingsPattern& secondary_pattern) {
// We only ever set origin-scoped exceptions which are of the form
// "https://<host>:443". That is a valid URL, so we can compare |host_filter|
// against its host.
GURL url = GURL(primary_pattern.ToString());
DCHECK(url.is_valid());
return std::move(host_filter).Run(url.host());
}
} // namespace
StatefulSSLHostStateDelegate::StatefulSSLHostStateDelegate(
content::BrowserContext* browser_context,
PrefService* pref_service,
HostContentSettingsMap* host_content_settings_map)
: clock_(new base::DefaultClock()),
browser_context_(browser_context),
pref_service_(pref_service),
host_content_settings_map_(host_content_settings_map),
recurrent_interstitial_threshold_for_testing(-1),
recurrent_interstitial_mode_for_testing(NOT_SET),
recurrent_interstitial_reset_time_for_testing(-1) {
}
StatefulSSLHostStateDelegate::~StatefulSSLHostStateDelegate() = default;
void StatefulSSLHostStateDelegate::RegisterProfilePrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterDictionaryPref(prefs::kRecurrentSSLInterstitial);
}
void StatefulSSLHostStateDelegate::AllowCert(
const std::string& host,
const net::X509Certificate& cert,
int error,
content::WebContents* web_contents) {
DCHECK(web_contents);
content::StoragePartition* storage_partition =
browser_context_->GetStoragePartition(
web_contents->GetMainFrame()->GetSiteInstance(),
false /* can_create */);
if (!storage_partition ||
storage_partition != browser_context_->GetDefaultStoragePartition()) {
// Decisions for non-default storage partitions are stored in memory only;
// see comment on declaration of
// |allowed_certs_for_non_default_storage_partitions_|.
auto allowed_cert =
AllowedCert(GetKey(cert, error), storage_partition->GetPath());
allowed_certs_for_non_default_storage_partitions_[host].insert(
allowed_cert);
return;
}
GURL url = GetSecureGURLForHost(host);
std::unique_ptr<base::Value> value(
host_content_settings_map_->GetWebsiteSetting(
url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));
if (!value.get() || !value->is_dict())
value = std::make_unique<base::Value>(base::Value::Type::DICTIONARY);
base::Value* cert_dict =
GetValidCertDecisionsDict(value.get(), CREATE_DICTIONARY_ENTRIES);
// 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;
value->SetKey(kSSLCertDecisionVersionKey,
base::Value(kDefaultSSLCertDecisionVersion));
cert_dict->SetKey(GetKey(cert, error), base::Value(ALLOWED));
// The map takes ownership of the value, so it is released in the call to
// SetWebsiteSettingDefaultScope.
host_content_settings_map_->SetWebsiteSettingDefaultScope(
url, GURL(), ContentSettingsType::SSL_CERT_DECISIONS, std::move(value));
}
void StatefulSSLHostStateDelegate::Clear(
base::RepeatingCallback<bool(const std::string&)> host_filter) {
// Convert host matching to content settings pattern matching. Content
// settings deletion is done synchronously on the UI thread, so we can use
// |host_filter| by reference.
HostContentSettingsMap::PatternSourcePredicate pattern_filter;
if (!host_filter.is_null()) {
pattern_filter =
base::BindRepeating(&HostFilterToPatternFilter, host_filter);
}
host_content_settings_map_->ClearSettingsForOneTypeWithPredicate(
ContentSettingsType::SSL_CERT_DECISIONS, base::Time(), base::Time::Max(),
pattern_filter);
}
content::SSLHostStateDelegate::CertJudgment
StatefulSSLHostStateDelegate::QueryPolicy(const std::string& host,
const net::X509Certificate& cert,
int error,
content::WebContents* web_contents) {
DCHECK(web_contents);
content::StoragePartition* storage_partition =
browser_context_->GetStoragePartition(
web_contents->GetMainFrame()->GetSiteInstance(),
false /* can_create */);
if (!storage_partition ||
storage_partition != browser_context_->GetDefaultStoragePartition()) {
if (allowed_certs_for_non_default_storage_partitions_.find(host) ==
allowed_certs_for_non_default_storage_partitions_.end()) {
return DENIED;
}
AllowedCert allowed_cert =
AllowedCert(GetKey(cert, error), storage_partition->GetPath());
if (base::Contains(allowed_certs_for_non_default_storage_partitions_[host],
allowed_cert)) {
return ALLOWED;
}
return DENIED;
}
GURL url = GetSecureGURLForHost(host);
std::unique_ptr<base::Value> value(
host_content_settings_map_->GetWebsiteSetting(
url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));
if (!value.get() || !value->is_dict())
return DENIED;
base::Value* cert_error_dict =
GetValidCertDecisionsDict(value.get(), DO_NOT_CREATE_DICTIONARY_ENTRIES);
if (!cert_error_dict) {
// This revoke is necessary to clear any old expired setting that may be
// lingering in the case that an old decision expried.
RevokeUserAllowExceptions(host);
return DENIED;
}
absl::optional<int> policy_decision =
cert_error_dict->FindIntKey(GetKey(cert, error));
// If a policy decision was successfully retrieved and it's a valid value of
// ALLOWED, return the valid value. Otherwise, return DENIED.
if (policy_decision.has_value() && policy_decision.value() == ALLOWED)
return ALLOWED;
return DENIED;
}
void StatefulSSLHostStateDelegate::HostRanInsecureContent(
const std::string& host,
int child_id,
InsecureContentType content_type) {
switch (content_type) {
case MIXED_CONTENT:
ran_mixed_content_hosts_.insert(BrokenHostEntry(host, child_id));
return;
case CERT_ERRORS_CONTENT:
ran_content_with_cert_errors_hosts_.insert(
BrokenHostEntry(host, child_id));
return;
}
}
bool StatefulSSLHostStateDelegate::DidHostRunInsecureContent(
const std::string& host,
int child_id,
InsecureContentType content_type) {
auto entry = BrokenHostEntry(host, child_id);
switch (content_type) {
case MIXED_CONTENT:
return base::Contains(ran_mixed_content_hosts_, entry);
case CERT_ERRORS_CONTENT:
return base::Contains(ran_content_with_cert_errors_hosts_, entry);
}
NOTREACHED();
return false;
}
// TODO(crbug.com/1218526): Hook up to content settings and expiration logic.
void StatefulSSLHostStateDelegate::AllowHttpForHost(const std::string& host) {
allow_http_hosts_.insert(host);
}
// TODO(crbug.com/1218526): Hook up to content settings and expiration logic.
bool StatefulSSLHostStateDelegate::IsHttpAllowedForHost(
const std::string& host) {
return base::Contains(allow_http_hosts_, host);
}
void StatefulSSLHostStateDelegate::RevokeUserAllowExceptions(
const std::string& host) {
GURL url = GetSecureGURLForHost(host);
host_content_settings_map_->SetWebsiteSettingDefaultScope(
url, GURL(), ContentSettingsType::SSL_CERT_DECISIONS, nullptr);
// Decisions for non-default storage partitions are stored separately in
// memory; delete those as well.
allowed_certs_for_non_default_storage_partitions_.erase(host);
}
bool StatefulSSLHostStateDelegate::HasAllowException(
const std::string& host,
content::WebContents* web_contents) {
DCHECK(web_contents);
content::StoragePartition* storage_partition =
browser_context_->GetStoragePartition(
web_contents->GetMainFrame()->GetSiteInstance(),
false /* can_create */);
if (!storage_partition ||
storage_partition != browser_context_->GetDefaultStoragePartition()) {
return allowed_certs_for_non_default_storage_partitions_.find(host) !=
allowed_certs_for_non_default_storage_partitions_.end();
}
GURL url = GetSecureGURLForHost(host);
const ContentSettingsPattern pattern =
ContentSettingsPattern::FromURLNoWildcard(url);
std::unique_ptr<base::Value> value(
host_content_settings_map_->GetWebsiteSetting(
url, url, ContentSettingsType::SSL_CERT_DECISIONS, nullptr));
if (!value.get() || !value->is_dict())
return false;
for (auto pair : value->DictItems()) {
if (!pair.second.is_int())
continue;
if (static_cast<CertJudgment>(pair.second.GetInt()) == ALLOWED)
return true;
}
return false;
}
// 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, RevokeUserAllowExceptionsHard is our solution for the rare case
// where it is necessary to revoke the preferences immediately. It does so by
// flushing idle sockets, thus it is a big hammer and should be wielded with
// extreme caution as it can have a big, negative impact on network performance.
void StatefulSSLHostStateDelegate::RevokeUserAllowExceptionsHard(
const std::string& host) {
RevokeUserAllowExceptions(host);
auto* network_context =
browser_context_->GetDefaultStoragePartition()->GetNetworkContext();
network_context->CloseIdleConnections(base::NullCallback());
}
void StatefulSSLHostStateDelegate::DidDisplayErrorPage(int error) {
if (error != net::ERR_CERT_SYMANTEC_LEGACY &&
error != net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED) {
return;
}
RecurrentInterstitialMode mode_param = GetRecurrentInterstitialMode();
const int threshold = GetRecurrentInterstitialThreshold();
if (mode_param ==
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::IN_MEMORY) {
const auto count_it = recurrent_errors_.find(error);
if (count_it == recurrent_errors_.end()) {
recurrent_errors_[error] = 1;
return;
}
if (count_it->second >= threshold) {
return;
}
recurrent_errors_[error] = count_it->second + 1;
} else if (mode_param ==
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::PREF) {
UpdateRecurrentInterstitialPref(pref_service_, clock_.get(), error,
threshold);
}
}
bool StatefulSSLHostStateDelegate::HasSeenRecurrentErrors(int error) const {
RecurrentInterstitialMode mode_param = GetRecurrentInterstitialMode();
const int threshold = GetRecurrentInterstitialThreshold();
if (mode_param ==
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::IN_MEMORY) {
const auto count_it = recurrent_errors_.find(error);
if (count_it == recurrent_errors_.end())
return false;
return count_it->second >= threshold;
} else if (mode_param ==
StatefulSSLHostStateDelegate::RecurrentInterstitialMode::PREF) {
return DoesRecurrentInterstitialPrefMeetThreshold(
pref_service_, clock_.get(), error, threshold,
GetRecurrentInterstitialResetTime());
}
return false;
}
void StatefulSSLHostStateDelegate::ResetRecurrentErrorCountForTesting() {
recurrent_errors_.clear();
DictionaryPrefUpdate pref_update(pref_service_,
prefs::kRecurrentSSLInterstitial);
pref_update->Clear();
}
void StatefulSSLHostStateDelegate::SetClockForTesting(
std::unique_ptr<base::Clock> clock) {
clock_ = std::move(clock);
}
void StatefulSSLHostStateDelegate::SetRecurrentInterstitialThresholdForTesting(
int threshold) {
recurrent_interstitial_threshold_for_testing = threshold;
}
void StatefulSSLHostStateDelegate::SetRecurrentInterstitialModeForTesting(
StatefulSSLHostStateDelegate::RecurrentInterstitialMode mode) {
recurrent_interstitial_mode_for_testing = mode;
}
void StatefulSSLHostStateDelegate::SetRecurrentInterstitialResetTimeForTesting(
int reset) {
recurrent_interstitial_reset_time_for_testing = reset;
}
int StatefulSSLHostStateDelegate::GetRecurrentInterstitialThreshold() const {
if (recurrent_interstitial_threshold_for_testing == -1) {
return kRecurrentInterstitialDefaultThreshold;
} else {
return recurrent_interstitial_threshold_for_testing;
}
}
int StatefulSSLHostStateDelegate::GetRecurrentInterstitialResetTime() const {
if (recurrent_interstitial_reset_time_for_testing == -1) {
return kRecurrentInterstitialDefaultResetTime;
} else {
return recurrent_interstitial_reset_time_for_testing;
}
}
StatefulSSLHostStateDelegate::RecurrentInterstitialMode
StatefulSSLHostStateDelegate::GetRecurrentInterstitialMode() const {
if (recurrent_interstitial_mode_for_testing == NOT_SET) {
return kRecurrentInterstitialDefaultMode;
} else {
return recurrent_interstitial_mode_for_testing;
}
}
// 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 |DO_NOT_CREATE_DICTIONARY_ENTRIES|,
// GetValidCertDecisionsDict will return nullptr if there is anything invalid
// about the setting, such as an invalid version or invalid value types (in
// addition to there not being any values in the dictionary). If create_entries
// is set to |CREATE_DICTIONARY_ENTRIES|, if no dictionary is found or the
// decisions are expired, a new dictionary will be created.
base::Value* StatefulSSLHostStateDelegate::GetValidCertDecisionsDict(
base::Value* dict,
CreateDictionaryEntriesDisposition create_entries) {
// Extract the version of the certificate decision structure from the content
// setting.
absl::optional<int> version = dict->FindIntKey(kSSLCertDecisionVersionKey);
if (!version) {
if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
return nullptr;
dict->SetIntKey(kSSLCertDecisionVersionKey, kDefaultSSLCertDecisionVersion);
version = absl::make_optional<int>(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 nullptr;
}
// 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;
const std::string* decision_expiration_string =
dict->FindStringKey(kSSLCertDecisionExpirationTimeKey);
if (decision_expiration_string) {
int64_t decision_expiration_int64;
if (!base::StringToInt64(*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 nullptr;
}
decision_expiration =
base::Time::FromInternalValue(decision_expiration_int64);
}
// Check to see if the user's certificate decision has expired.
// - Expired and |create_entries| is DO_NOT_CREATE_DICTIONARY_ENTRIES, return
// nullptr.
// - Expired and |create_entries| is CREATE_DICTIONARY_ENTRIES, update the
// expiration time.
if (decision_expiration.ToInternalValue() <= now.ToInternalValue()) {
if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
return nullptr;
expired = true;
base::Time expiration_time =
now + base::TimeDelta::FromSeconds(kDeltaDefaultExpirationInSeconds);
// Unfortunately, JSON (and thus content settings) doesn't support int64_t
// values, only doubles. Since this mildly depends on precision, it is
// better to store the value as a string.
dict->SetStringKey(kSSLCertDecisionExpirationTimeKey,
base::NumberToString(expiration_time.ToInternalValue()));
}
// Extract the map of certificate fingerprints to errors from the setting.
base::Value* cert_error_dict =
dict->FindDictKey(kSSLCertDecisionCertErrorMapKey);
if (expired || !cert_error_dict) {
if (create_entries == DO_NOT_CREATE_DICTIONARY_ENTRIES)
return nullptr;
cert_error_dict = dict->SetKey(kSSLCertDecisionCertErrorMapKey,
base::Value(base::Value::Type::DICTIONARY));
}
return cert_error_dict;
}