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