Index: chrome/browser/ssl/ssl_error_classification.cc |
diff --git a/chrome/browser/ssl/ssl_error_classification.cc b/chrome/browser/ssl/ssl_error_classification.cc |
index c81c88794f06e42f1a8adbbf0451a57a347dd0f5..0f8911dac843bcaaf7ea553afdbadfc743ac6b52 100644 |
--- a/chrome/browser/ssl/ssl_error_classification.cc |
+++ b/chrome/browser/ssl/ssl_error_classification.cc |
@@ -2,15 +2,22 @@ |
// Use of this source code is governed by a BSD-style license that can be |
// found in the LICENSE file. |
+#include <vector> |
+ |
#include "chrome/browser/ssl/ssl_error_classification.h" |
#include "base/build_time.h" |
#include "base/metrics/field_trial.h" |
#include "base/metrics/histogram.h" |
+#include "base/strings/string_split.h" |
+#include "base/strings/utf_string_conversions.h" |
#include "base/time/time.h" |
-#include "chrome/browser/browser_process.h" |
-#include "components/network_time/network_time_tracker.h" |
+#include "chrome/browser/ssl/ssl_error_info.h" |
+#include "net/base/net_util.h" |
+#include "net/base/registry_controlled_domains/registry_controlled_domain.h" |
+#include "net/cert/x509_cert_types.h" |
#include "net/cert/x509_certificate.h" |
+#include "url/gurl.h" |
using base::Time; |
using base::TimeTicks; |
@@ -26,9 +33,18 @@ namespace { |
enum SSLInterstitialCause { |
CLOCK_PAST, |
CLOCK_FUTURE, |
+ WWW_SUBDOMAIN_MATCH, |
+ SUBDOMAIN_MATCH, |
+ SUBDOMAIN_INVERSE_MATCH, |
+ SUBDOMAIN_OUTSIDE_WILDCARD, |
+ HOST_NAME_NOT_KNOWN_TLD, |
UNUSED_INTERSTITIAL_CAUSE_ENTRY, |
}; |
+// Scores/weights which will be constant through all the SSL error types. |
+static const float kServerWeight = 0.5f; |
+static const float kClientWeight = 0.5f; |
+ |
void RecordSSLInterstitialCause(bool overridable, SSLInterstitialCause event) { |
if (overridable) { |
UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.cause.overridable", event, |
@@ -42,33 +58,38 @@ void RecordSSLInterstitialCause(bool overridable, SSLInterstitialCause event) { |
} // namespace |
SSLErrorClassification::SSLErrorClassification( |
- base::Time current_time, |
+ const base::Time& current_time, |
+ const GURL& url, |
const net::X509Certificate& cert) |
: current_time_(current_time), |
+ request_url_(url), |
cert_(cert) { } |
SSLErrorClassification::~SSLErrorClassification() { } |
-float SSLErrorClassification::InvalidDateSeverityScore() const { |
- // Client-side characterisitics. Check whether the system's clock is wrong or |
- // not and whether the user has encountered this error before or not. |
+float SSLErrorClassification::InvalidDateSeverityScore( |
+ int cert_error) const { |
+ SSLErrorInfo::ErrorType type = |
+ SSLErrorInfo::NetErrorToErrorType(cert_error); |
+ DCHECK(type == SSLErrorInfo::CERT_DATE_INVALID); |
+ // Client-side characteristics. Check whether or not the system's clock is |
+ // wrong and whether or not the user has already encountered this error |
+ // before. |
float severity_date_score = 0.0f; |
- static const float kClientWeight = 0.5f; |
+ static const float kCertificateExpiredWeight = 0.3f; |
+ static const float kNotYetValidWeight = 0.2f; |
+ |
static const float kSystemClockWeight = 0.75f; |
static const float kSystemClockWrongWeight = 0.1f; |
static const float kSystemClockRightWeight = 1.0f; |
- static const float kServerWeight = 0.5f; |
- static const float kCertificateExpiredWeight = 0.3f; |
- static const float kNotYetValidWeight = 0.2f; |
- |
if (IsUserClockInThePast(current_time_) || |
IsUserClockInTheFuture(current_time_)) { |
- severity_date_score = kClientWeight * kSystemClockWeight * |
+ severity_date_score += kClientWeight * kSystemClockWeight * |
kSystemClockWrongWeight; |
} else { |
- severity_date_score = kClientWeight * kSystemClockWeight * |
+ severity_date_score += kClientWeight * kSystemClockWeight * |
kSystemClockRightWeight; |
} |
// TODO(radhikabhar): (crbug.com/393262) Check website settings. |
@@ -85,6 +106,75 @@ float SSLErrorClassification::InvalidDateSeverityScore() const { |
return severity_date_score; |
} |
+float SSLErrorClassification::InvalidCommonNameSeverityScore( |
+ int cert_error) const { |
+ SSLErrorInfo::ErrorType type = |
+ SSLErrorInfo::NetErrorToErrorType(cert_error); |
+ DCHECK(type == SSLErrorInfo::CERT_COMMON_NAME_INVALID); |
+ float severity_name_score = 0.0f; |
+ |
+ static const float kWWWDifferenceWeight = 0.3f; |
+ static const float kSubDomainWeight = 0.2f; |
+ static const float kSubDomainInverseWeight = 1.0f; |
+ |
+ std::string host_name = request_url_.host(); |
+ if (IsHostNameKnownTLD(host_name)) { |
+ Tokens host_name_tokens = Tokenize(host_name); |
+ if (IsWWWSubDomainMatch()) |
+ severity_name_score += kServerWeight * kWWWDifferenceWeight; |
+ if (IsSubDomainOutsideWildcard(host_name_tokens)) |
+ severity_name_score += kServerWeight * kWWWDifferenceWeight; |
+ |
+ std::vector<std::string> dns_names; |
+ cert_.GetDNSNames(&dns_names); |
+ std::vector<Tokens> dns_name_tokens = GetTokenizedDNSNames(dns_names); |
+ if (NameUnderAnyNames(host_name_tokens, dns_name_tokens)) |
+ severity_name_score += kServerWeight * kSubDomainWeight; |
+ // Inverse case is more likely to be a MITM attack. |
+ if (AnyNamesUnderName(dns_name_tokens, host_name_tokens)) |
+ severity_name_score += kServerWeight * kSubDomainInverseWeight; |
+ } |
+ return severity_name_score; |
+} |
+ |
+void SSLErrorClassification::RecordUMAStatistics(bool overridable, |
+ int cert_error) { |
+ SSLErrorInfo::ErrorType type = |
+ SSLErrorInfo::NetErrorToErrorType(cert_error); |
+ switch (type) { |
+ case SSLErrorInfo::CERT_DATE_INVALID: { |
+ if (IsUserClockInThePast(base::Time::NowFromSystemTime())) |
+ RecordSSLInterstitialCause(overridable, CLOCK_PAST); |
+ if (IsUserClockInTheFuture(base::Time::NowFromSystemTime())) |
+ RecordSSLInterstitialCause(overridable, CLOCK_FUTURE); |
+ break; |
+ } |
+ case SSLErrorInfo::CERT_COMMON_NAME_INVALID: { |
+ std::string host_name = request_url_.host(); |
+ if (IsHostNameKnownTLD(host_name)) { |
+ Tokens host_name_tokens = Tokenize(host_name); |
+ if (IsWWWSubDomainMatch()) |
+ RecordSSLInterstitialCause(overridable, WWW_SUBDOMAIN_MATCH); |
+ if (IsSubDomainOutsideWildcard(host_name_tokens)) |
+ RecordSSLInterstitialCause(overridable, SUBDOMAIN_OUTSIDE_WILDCARD); |
+ std::vector<std::string> dns_names; |
+ cert_.GetDNSNames(&dns_names); |
+ std::vector<Tokens> dns_name_tokens = GetTokenizedDNSNames(dns_names); |
+ if (NameUnderAnyNames(host_name_tokens, dns_name_tokens)) |
+ RecordSSLInterstitialCause(overridable, SUBDOMAIN_MATCH); |
+ if (AnyNamesUnderName(dns_name_tokens, host_name_tokens)) |
+ RecordSSLInterstitialCause(overridable, SUBDOMAIN_INVERSE_MATCH); |
+ } else { |
+ RecordSSLInterstitialCause(overridable, HOST_NAME_NOT_KNOWN_TLD); |
+ } |
+ break; |
+ } |
+ default: { |
+ break; |
+ } |
+ } |
+} |
+ |
base::TimeDelta SSLErrorClassification::TimePassedSinceExpiry() const { |
base::TimeDelta delta = current_time_ - cert_.valid_expiry(); |
return delta; |
@@ -106,14 +196,15 @@ float SSLErrorClassification::CalculateScoreTimePassedSinceExpiry() const { |
return kLowThresholdWeight; |
} |
-bool SSLErrorClassification::IsUserClockInThePast(base::Time time_now) { |
+bool SSLErrorClassification::IsUserClockInThePast(const base::Time& time_now) { |
base::Time build_time = base::GetBuildTime(); |
if (time_now < build_time - base::TimeDelta::FromDays(2)) |
return true; |
return false; |
} |
-bool SSLErrorClassification::IsUserClockInTheFuture(base::Time time_now) { |
+bool SSLErrorClassification::IsUserClockInTheFuture( |
+ const base::Time& time_now) { |
base::Time build_time = base::GetBuildTime(); |
if (time_now > build_time + base::TimeDelta::FromDays(365)) |
return true; |
@@ -130,9 +221,148 @@ bool SSLErrorClassification::IsWindowsVersionSP3OrLower() { |
return false; |
} |
-void SSLErrorClassification::RecordUMAStatistics(bool overridable) { |
- if (IsUserClockInThePast(base::Time::NowFromSystemTime())) |
- RecordSSLInterstitialCause(overridable, CLOCK_PAST); |
- if (IsUserClockInTheFuture(base::Time::NowFromSystemTime())) |
- RecordSSLInterstitialCause(overridable, CLOCK_FUTURE); |
+bool SSLErrorClassification::IsHostNameKnownTLD(const std::string& host_name) { |
+ size_t tld_length = |
+ net::registry_controlled_domains::GetRegistryLength( |
+ host_name, |
+ net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, |
+ net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); |
+ if (tld_length == 0 || tld_length == std::string::npos) |
+ return false; |
+ return true; |
+} |
+ |
+std::vector<SSLErrorClassification::Tokens> SSLErrorClassification:: |
+GetTokenizedDNSNames(const std::vector<std::string>& dns_names) { |
+ std::vector<std::vector<std::string>> dns_name_tokens; |
+ for (size_t i = 0; i < dns_names.size(); ++i) { |
+ std::vector<std::string> dns_name_token_single; |
+ if (dns_names[i].empty() || dns_names[i].find('\0') != std::string::npos |
+ || !(IsHostNameKnownTLD(dns_names[i]))) { |
+ dns_name_token_single.push_back(std::string()); |
+ } else { |
+ dns_name_token_single = Tokenize(dns_names[i]); |
+ } |
+ dns_name_tokens.push_back(dns_name_token_single); |
+ } |
+ return dns_name_tokens; |
+} |
+ |
+size_t SSLErrorClassification::FindSubDomainDifference( |
+ const Tokens& potential_subdomain, const Tokens& parent) const { |
+ // A check to ensure that the number of tokens in the tokenized_parent is |
+ // less than the tokenized_potential_subdomain. |
+ if (parent.size() >= potential_subdomain.size()) |
+ return 0; |
+ |
+ size_t tokens_match = 0; |
+ size_t diff_size = potential_subdomain.size() - parent.size(); |
+ for (size_t i = 0; i < parent.size(); ++i) { |
+ if (parent[i] == potential_subdomain[i + diff_size]) |
+ tokens_match++; |
+ } |
+ if (tokens_match == parent.size()) |
+ return diff_size; |
+ return 0; |
+} |
+ |
+SSLErrorClassification::Tokens SSLErrorClassification:: |
+Tokenize(const std::string& name) { |
+ Tokens name_tokens; |
+ base::SplitStringDontTrim(name, '.', &name_tokens); |
+ return name_tokens; |
+} |
+ |
+// We accept the inverse case for www for historical reasons. |
+bool SSLErrorClassification::IsWWWSubDomainMatch() const { |
+ std::string host_name = request_url_.host(); |
+ if (IsHostNameKnownTLD(host_name)) { |
+ std::vector<std::string> dns_names; |
+ cert_.GetDNSNames(&dns_names); |
+ bool result = false; |
+ // Need to account for all possible domains given in the SSL certificate. |
+ for (size_t i = 0; i < dns_names.size(); ++i) { |
+ if (dns_names[i].empty() || dns_names[i].find('\0') != std::string::npos |
+ || dns_names[i].length() == host_name.length() |
+ || !(IsHostNameKnownTLD(dns_names[i]))) { |
+ result = result || false; |
+ } else if (dns_names[i].length() > host_name.length()) { |
+ result = result || |
+ net::StripWWW(base::ASCIIToUTF16(dns_names[i])) == |
+ base::ASCIIToUTF16(host_name); |
+ } else { |
+ result = result || |
+ net::StripWWW(base::ASCIIToUTF16(host_name)) == |
+ base::ASCIIToUTF16(dns_names[i]); |
+ } |
+ } |
+ return result; |
+ } |
+ return false; |
+} |
+ |
+bool SSLErrorClassification::NameUnderAnyNames( |
+ const Tokens& child, |
+ const std::vector<Tokens>& potential_parents) const { |
+ bool result = false; |
+ // Need to account for all the possible domains given in the SSL certificate. |
+ for (size_t i = 0; i < potential_parents.size(); ++i) { |
+ if (potential_parents[i].empty() || |
+ potential_parents[i].size() >= child.size()) { |
+ result = result || false; |
+ } else { |
+ size_t domain_diff = FindSubDomainDifference(child, |
+ potential_parents[i]); |
+ if (domain_diff == 1 && child[0] != "www") |
+ result = result || true; |
+ } |
+ } |
+ return result; |
+} |
+ |
+bool SSLErrorClassification::AnyNamesUnderName( |
+ const std::vector<Tokens>& potential_children, |
+ const Tokens& parent) const { |
+ bool result = false; |
+ // Need to account for all the possible domains given in the SSL certificate. |
+ for (size_t i = 0; i < potential_children.size(); ++i) { |
+ if (potential_children[i].empty() || |
+ potential_children[i].size() <= parent.size()) { |
+ result = result || false; |
+ } else { |
+ size_t domain_diff = FindSubDomainDifference(potential_children[i], |
+ parent); |
+ if (domain_diff == 1 && potential_children[i][0] != "www") |
+ result = result || true; |
+ } |
+ } |
+ return result; |
+} |
+ |
+bool SSLErrorClassification::IsSubDomainOutsideWildcard( |
+ const Tokens& host_name_tokens) const { |
+ std::string host_name = request_url_.host(); |
+ std::vector<std::string> dns_names; |
+ cert_.GetDNSNames(&dns_names); |
+ bool result = false; |
+ |
+ // This method requires that the host name be longer than the dns name on |
+ // the certificate. |
+ for (size_t i = 0; i < dns_names.size(); ++i) { |
+ const std::string& name = dns_names[i]; |
+ if (name.length() < 2 || name.length() >= host_name.length() || |
+ name.find('\0') != std::string::npos || |
+ !IsHostNameKnownTLD(name) |
+ || name[0] != '*' || name[1] != '.') { |
+ continue; |
+ } |
+ |
+ // Move past the "*.". |
+ std::string extracted_dns_name = name.substr(2); |
+ if (FindSubDomainDifference( |
+ host_name_tokens, Tokenize(extracted_dns_name)) == 2) { |
+ return true; |
+ } |
+ } |
+ return result; |
} |