ssl_error_classification.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 <vector> 6 7#include "chrome/browser/ssl/ssl_error_classification.h" 8 9#include "base/build_time.h" 10#include "base/metrics/field_trial.h" 11#include "base/metrics/histogram.h" 12#include "base/strings/string_split.h" 13#include "base/strings/utf_string_conversions.h" 14#include "base/time/time.h" 15#include "chrome/browser/ssl/ssl_error_info.h" 16#include "net/base/net_util.h" 17#include "net/base/registry_controlled_domains/registry_controlled_domain.h" 18#include "net/cert/x509_cert_types.h" 19#include "net/cert/x509_certificate.h" 20#include "url/gurl.h" 21 22using base::Time; 23using base::TimeTicks; 24using base::TimeDelta; 25 26#if defined(OS_WIN) 27#include "base/win/windows_version.h" 28#endif 29 30namespace { 31 32// Events for UMA. Do not reorder or change! 33enum SSLInterstitialCause { 34 CLOCK_PAST, 35 CLOCK_FUTURE, 36 WWW_SUBDOMAIN_MATCH, 37 SUBDOMAIN_MATCH, 38 SUBDOMAIN_INVERSE_MATCH, 39 SUBDOMAIN_OUTSIDE_WILDCARD, 40 HOST_NAME_NOT_KNOWN_TLD, 41 UNUSED_INTERSTITIAL_CAUSE_ENTRY, 42}; 43 44// Scores/weights which will be constant through all the SSL error types. 45static const float kServerWeight = 0.5f; 46static const float kClientWeight = 0.5f; 47 48void RecordSSLInterstitialCause(bool overridable, SSLInterstitialCause event) { 49 if (overridable) { 50 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.cause.overridable", event, 51 UNUSED_INTERSTITIAL_CAUSE_ENTRY); 52 } else { 53 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.cause.nonoverridable", event, 54 UNUSED_INTERSTITIAL_CAUSE_ENTRY); 55 } 56} 57 58} // namespace 59 60SSLErrorClassification::SSLErrorClassification( 61 const base::Time& current_time, 62 const GURL& url, 63 const net::X509Certificate& cert) 64 : current_time_(current_time), 65 request_url_(url), 66 cert_(cert) { } 67 68SSLErrorClassification::~SSLErrorClassification() { } 69 70float SSLErrorClassification::InvalidDateSeverityScore( 71 int cert_error) const { 72 SSLErrorInfo::ErrorType type = 73 SSLErrorInfo::NetErrorToErrorType(cert_error); 74 DCHECK(type == SSLErrorInfo::CERT_DATE_INVALID); 75 // Client-side characteristics. Check whether or not the system's clock is 76 // wrong and whether or not the user has already encountered this error 77 // before. 78 float severity_date_score = 0.0f; 79 80 static const float kCertificateExpiredWeight = 0.3f; 81 static const float kNotYetValidWeight = 0.2f; 82 83 static const float kSystemClockWeight = 0.75f; 84 static const float kSystemClockWrongWeight = 0.1f; 85 static const float kSystemClockRightWeight = 1.0f; 86 87 if (IsUserClockInThePast(current_time_) || 88 IsUserClockInTheFuture(current_time_)) { 89 severity_date_score += kClientWeight * kSystemClockWeight * 90 kSystemClockWrongWeight; 91 } else { 92 severity_date_score += kClientWeight * kSystemClockWeight * 93 kSystemClockRightWeight; 94 } 95 // TODO(radhikabhar): (crbug.com/393262) Check website settings. 96 97 // Server-side characteristics. Check whether the certificate has expired or 98 // is not yet valid. If the certificate has expired then factor the time which 99 // has passed since expiry. 100 if (cert_.HasExpired()) { 101 severity_date_score += kServerWeight * kCertificateExpiredWeight * 102 CalculateScoreTimePassedSinceExpiry(); 103 } 104 if (current_time_ < cert_.valid_start()) 105 severity_date_score += kServerWeight * kNotYetValidWeight; 106 return severity_date_score; 107} 108 109float SSLErrorClassification::InvalidCommonNameSeverityScore( 110 int cert_error) const { 111 SSLErrorInfo::ErrorType type = 112 SSLErrorInfo::NetErrorToErrorType(cert_error); 113 DCHECK(type == SSLErrorInfo::CERT_COMMON_NAME_INVALID); 114 float severity_name_score = 0.0f; 115 116 static const float kWWWDifferenceWeight = 0.3f; 117 static const float kSubDomainWeight = 0.2f; 118 static const float kSubDomainInverseWeight = 1.0f; 119 120 std::string host_name = request_url_.host(); 121 if (IsHostNameKnownTLD(host_name)) { 122 Tokens host_name_tokens = Tokenize(host_name); 123 if (IsWWWSubDomainMatch()) 124 severity_name_score += kServerWeight * kWWWDifferenceWeight; 125 if (IsSubDomainOutsideWildcard(host_name_tokens)) 126 severity_name_score += kServerWeight * kWWWDifferenceWeight; 127 128 std::vector<std::string> dns_names; 129 cert_.GetDNSNames(&dns_names); 130 std::vector<Tokens> dns_name_tokens = GetTokenizedDNSNames(dns_names); 131 if (NameUnderAnyNames(host_name_tokens, dns_name_tokens)) 132 severity_name_score += kServerWeight * kSubDomainWeight; 133 // Inverse case is more likely to be a MITM attack. 134 if (AnyNamesUnderName(dns_name_tokens, host_name_tokens)) 135 severity_name_score += kServerWeight * kSubDomainInverseWeight; 136 } 137 return severity_name_score; 138} 139 140void SSLErrorClassification::RecordUMAStatistics(bool overridable, 141 int cert_error) { 142 SSLErrorInfo::ErrorType type = 143 SSLErrorInfo::NetErrorToErrorType(cert_error); 144 switch (type) { 145 case SSLErrorInfo::CERT_DATE_INVALID: { 146 if (IsUserClockInThePast(base::Time::NowFromSystemTime())) 147 RecordSSLInterstitialCause(overridable, CLOCK_PAST); 148 if (IsUserClockInTheFuture(base::Time::NowFromSystemTime())) 149 RecordSSLInterstitialCause(overridable, CLOCK_FUTURE); 150 break; 151 } 152 case SSLErrorInfo::CERT_COMMON_NAME_INVALID: { 153 std::string host_name = request_url_.host(); 154 if (IsHostNameKnownTLD(host_name)) { 155 Tokens host_name_tokens = Tokenize(host_name); 156 if (IsWWWSubDomainMatch()) 157 RecordSSLInterstitialCause(overridable, WWW_SUBDOMAIN_MATCH); 158 if (IsSubDomainOutsideWildcard(host_name_tokens)) 159 RecordSSLInterstitialCause(overridable, SUBDOMAIN_OUTSIDE_WILDCARD); 160 std::vector<std::string> dns_names; 161 cert_.GetDNSNames(&dns_names); 162 std::vector<Tokens> dns_name_tokens = GetTokenizedDNSNames(dns_names); 163 if (NameUnderAnyNames(host_name_tokens, dns_name_tokens)) 164 RecordSSLInterstitialCause(overridable, SUBDOMAIN_MATCH); 165 if (AnyNamesUnderName(dns_name_tokens, host_name_tokens)) 166 RecordSSLInterstitialCause(overridable, SUBDOMAIN_INVERSE_MATCH); 167 } else { 168 RecordSSLInterstitialCause(overridable, HOST_NAME_NOT_KNOWN_TLD); 169 } 170 break; 171 } 172 default: { 173 break; 174 } 175 } 176} 177 178base::TimeDelta SSLErrorClassification::TimePassedSinceExpiry() const { 179 base::TimeDelta delta = current_time_ - cert_.valid_expiry(); 180 return delta; 181} 182 183float SSLErrorClassification::CalculateScoreTimePassedSinceExpiry() const { 184 base::TimeDelta delta = TimePassedSinceExpiry(); 185 int64 time_passed = delta.InDays(); 186 const int64 kHighThreshold = 7; 187 const int64 kLowThreshold = 4; 188 static const float kHighThresholdWeight = 0.4f; 189 static const float kMediumThresholdWeight = 0.3f; 190 static const float kLowThresholdWeight = 0.2f; 191 if (time_passed >= kHighThreshold) 192 return kHighThresholdWeight; 193 else if (time_passed >= kLowThreshold) 194 return kMediumThresholdWeight; 195 else 196 return kLowThresholdWeight; 197} 198 199bool SSLErrorClassification::IsUserClockInThePast(const base::Time& time_now) { 200 base::Time build_time = base::GetBuildTime(); 201 if (time_now < build_time - base::TimeDelta::FromDays(2)) 202 return true; 203 return false; 204} 205 206bool SSLErrorClassification::IsUserClockInTheFuture( 207 const base::Time& time_now) { 208 base::Time build_time = base::GetBuildTime(); 209 if (time_now > build_time + base::TimeDelta::FromDays(365)) 210 return true; 211 return false; 212} 213 214bool SSLErrorClassification::IsWindowsVersionSP3OrLower() { 215#if defined(OS_WIN) 216 const base::win::OSInfo* os_info = base::win::OSInfo::GetInstance(); 217 base::win::OSInfo::ServicePack service_pack = os_info->service_pack(); 218 if (os_info->version() < base::win::VERSION_VISTA && service_pack.major < 3) 219 return true; 220#endif 221 return false; 222} 223 224bool SSLErrorClassification::IsHostNameKnownTLD(const std::string& host_name) { 225 size_t tld_length = 226 net::registry_controlled_domains::GetRegistryLength( 227 host_name, 228 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES, 229 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES); 230 if (tld_length == 0 || tld_length == std::string::npos) 231 return false; 232 return true; 233} 234 235std::vector<SSLErrorClassification::Tokens> SSLErrorClassification:: 236GetTokenizedDNSNames(const std::vector<std::string>& dns_names) { 237 std::vector<std::vector<std::string>> dns_name_tokens; 238 for (size_t i = 0; i < dns_names.size(); ++i) { 239 std::vector<std::string> dns_name_token_single; 240 if (dns_names[i].empty() || dns_names[i].find('\0') != std::string::npos 241 || !(IsHostNameKnownTLD(dns_names[i]))) { 242 dns_name_token_single.push_back(std::string()); 243 } else { 244 dns_name_token_single = Tokenize(dns_names[i]); 245 } 246 dns_name_tokens.push_back(dns_name_token_single); 247 } 248 return dns_name_tokens; 249} 250 251size_t SSLErrorClassification::FindSubDomainDifference( 252 const Tokens& potential_subdomain, const Tokens& parent) const { 253 // A check to ensure that the number of tokens in the tokenized_parent is 254 // less than the tokenized_potential_subdomain. 255 if (parent.size() >= potential_subdomain.size()) 256 return 0; 257 258 size_t tokens_match = 0; 259 size_t diff_size = potential_subdomain.size() - parent.size(); 260 for (size_t i = 0; i < parent.size(); ++i) { 261 if (parent[i] == potential_subdomain[i + diff_size]) 262 tokens_match++; 263 } 264 if (tokens_match == parent.size()) 265 return diff_size; 266 return 0; 267} 268 269SSLErrorClassification::Tokens SSLErrorClassification:: 270Tokenize(const std::string& name) { 271 Tokens name_tokens; 272 base::SplitStringDontTrim(name, '.', &name_tokens); 273 return name_tokens; 274} 275 276// We accept the inverse case for www for historical reasons. 277bool SSLErrorClassification::IsWWWSubDomainMatch() const { 278 std::string host_name = request_url_.host(); 279 if (IsHostNameKnownTLD(host_name)) { 280 std::vector<std::string> dns_names; 281 cert_.GetDNSNames(&dns_names); 282 bool result = false; 283 // Need to account for all possible domains given in the SSL certificate. 284 for (size_t i = 0; i < dns_names.size(); ++i) { 285 if (dns_names[i].empty() || dns_names[i].find('\0') != std::string::npos 286 || dns_names[i].length() == host_name.length() 287 || !(IsHostNameKnownTLD(dns_names[i]))) { 288 result = result || false; 289 } else if (dns_names[i].length() > host_name.length()) { 290 result = result || 291 net::StripWWW(base::ASCIIToUTF16(dns_names[i])) == 292 base::ASCIIToUTF16(host_name); 293 } else { 294 result = result || 295 net::StripWWW(base::ASCIIToUTF16(host_name)) == 296 base::ASCIIToUTF16(dns_names[i]); 297 } 298 } 299 return result; 300 } 301 return false; 302} 303 304bool SSLErrorClassification::NameUnderAnyNames( 305 const Tokens& child, 306 const std::vector<Tokens>& potential_parents) const { 307 bool result = false; 308 // Need to account for all the possible domains given in the SSL certificate. 309 for (size_t i = 0; i < potential_parents.size(); ++i) { 310 if (potential_parents[i].empty() || 311 potential_parents[i].size() >= child.size()) { 312 result = result || false; 313 } else { 314 size_t domain_diff = FindSubDomainDifference(child, 315 potential_parents[i]); 316 if (domain_diff == 1 && child[0] != "www") 317 result = result || true; 318 } 319 } 320 return result; 321} 322 323bool SSLErrorClassification::AnyNamesUnderName( 324 const std::vector<Tokens>& potential_children, 325 const Tokens& parent) const { 326 bool result = false; 327 // Need to account for all the possible domains given in the SSL certificate. 328 for (size_t i = 0; i < potential_children.size(); ++i) { 329 if (potential_children[i].empty() || 330 potential_children[i].size() <= parent.size()) { 331 result = result || false; 332 } else { 333 size_t domain_diff = FindSubDomainDifference(potential_children[i], 334 parent); 335 if (domain_diff == 1 && potential_children[i][0] != "www") 336 result = result || true; 337 } 338 } 339 return result; 340} 341 342bool SSLErrorClassification::IsSubDomainOutsideWildcard( 343 const Tokens& host_name_tokens) const { 344 std::string host_name = request_url_.host(); 345 std::vector<std::string> dns_names; 346 cert_.GetDNSNames(&dns_names); 347 bool result = false; 348 349 // This method requires that the host name be longer than the dns name on 350 // the certificate. 351 for (size_t i = 0; i < dns_names.size(); ++i) { 352 const std::string& name = dns_names[i]; 353 if (name.length() < 2 || name.length() >= host_name.length() || 354 name.find('\0') != std::string::npos || 355 !IsHostNameKnownTLD(name) 356 || name[0] != '*' || name[1] != '.') { 357 continue; 358 } 359 360 // Move past the "*.". 361 std::string extracted_dns_name = name.substr(2); 362 if (FindSubDomainDifference( 363 host_name_tokens, Tokenize(extracted_dns_name)) == 2) { 364 return true; 365 } 366 } 367 return result; 368} 369