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.
7 #include "chrome/browser/ssl/ssl_error_classification.h"
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/browser_process.h"
16 #include "chrome/browser/chrome_notification_types.h"
17 #include "chrome/browser/profiles/profile.h"
18 #include "chrome/browser/ssl/ssl_error_info.h"
19 #include "content/public/browser/notification_service.h"
20 #include "content/public/browser/web_contents.h"
21 #include "net/base/net_util.h"
22 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
23 #include "net/cert/x509_cert_types.h"
24 #include "net/cert/x509_certificate.h"
27 #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION)
28 #include "chrome/browser/captive_portal/captive_portal_service.h"
29 #include "chrome/browser/captive_portal/captive_portal_service_factory.h"
33 #include "base/win/win_util.h"
34 #include "base/win/windows_version.h"
38 using base::TimeTicks
;
39 using base::TimeDelta
;
43 // Events for UMA. Do not reorder or change!
44 enum SSLInterstitialCause
{
49 SUBDOMAIN_INVERSE_MATCH
,
50 SUBDOMAIN_OUTSIDE_WILDCARD
,
51 HOST_NAME_NOT_KNOWN_TLD
,
52 LIKELY_MULTI_TENANT_HOSTING
,
55 AUTHORITY_ERROR_CAPTIVE_PORTAL
,
58 UNUSED_INTERSTITIAL_CAUSE_ENTRY
,
61 // Events for UMA. Do not reorder or change!
62 enum SSLInterstitialCauseCaptivePortal
{
64 CAPTIVE_PORTAL_DETECTION_ENABLED
,
65 CAPTIVE_PORTAL_DETECTION_ENABLED_OVERRIDABLE
,
66 CAPTIVE_PORTAL_PROBE_COMPLETED
,
67 CAPTIVE_PORTAL_PROBE_COMPLETED_OVERRIDABLE
,
68 CAPTIVE_PORTAL_NO_RESPONSE
,
69 CAPTIVE_PORTAL_NO_RESPONSE_OVERRIDABLE
,
70 CAPTIVE_PORTAL_DETECTED
,
71 CAPTIVE_PORTAL_DETECTED_OVERRIDABLE
,
72 UNUSED_CAPTIVE_PORTAL_EVENT
,
75 void RecordSSLInterstitialCause(bool overridable
, SSLInterstitialCause event
) {
77 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.cause.overridable", event
,
78 UNUSED_INTERSTITIAL_CAUSE_ENTRY
);
80 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.cause.nonoverridable", event
,
81 UNUSED_INTERSTITIAL_CAUSE_ENTRY
);
85 #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION)
86 void RecordCaptivePortalEventStats(SSLInterstitialCauseCaptivePortal event
) {
87 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.captive_portal",
89 UNUSED_CAPTIVE_PORTAL_EVENT
);
93 int GetLevensteinDistance(const std::string
& str1
,
94 const std::string
& str2
) {
101 std::vector
<int> kFirstRow(str2
.size() + 1, 0);
102 std::vector
<int> kSecondRow(str2
.size() + 1, 0);
104 for (size_t i
= 0; i
< kFirstRow
.size(); ++i
)
106 for (size_t i
= 0; i
< str1
.size(); ++i
) {
107 kSecondRow
[0] = i
+ 1;
108 for (size_t j
= 0; j
< str2
.size(); ++j
) {
109 int cost
= str1
[i
] == str2
[j
] ? 0 : 1;
110 kSecondRow
[j
+1] = std::min(std::min(
111 kSecondRow
[j
] + 1, kFirstRow
[j
+ 1] + 1), kFirstRow
[j
] + cost
);
113 for (size_t j
= 0; j
< kFirstRow
.size(); j
++)
114 kFirstRow
[j
] = kSecondRow
[j
];
116 return kSecondRow
[str2
.size()];
121 SSLErrorClassification::SSLErrorClassification(
122 content::WebContents
* web_contents
,
123 const base::Time
& current_time
,
126 const net::X509Certificate
& cert
)
127 : web_contents_(web_contents
),
128 current_time_(current_time
),
130 cert_error_(cert_error
),
132 captive_portal_detection_enabled_(false),
133 captive_portal_probe_completed_(false),
134 captive_portal_no_response_(false),
135 captive_portal_detected_(false) {
136 #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION)
137 Profile
* profile
= Profile::FromBrowserContext(
138 web_contents_
->GetBrowserContext());
139 captive_portal_detection_enabled_
=
140 CaptivePortalServiceFactory::GetForProfile(profile
)->enabled();
142 chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT
,
143 content::Source
<Profile
>(profile
));
147 SSLErrorClassification::~SSLErrorClassification() { }
149 void SSLErrorClassification::RecordCaptivePortalUMAStatistics(
150 bool overridable
) const {
151 #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION)
152 RecordCaptivePortalEventStats(CAPTIVE_PORTAL_ALL
);
153 if (captive_portal_detection_enabled_
)
154 RecordCaptivePortalEventStats(
156 CAPTIVE_PORTAL_DETECTION_ENABLED_OVERRIDABLE
:
157 CAPTIVE_PORTAL_DETECTION_ENABLED
);
158 if (captive_portal_probe_completed_
)
159 RecordCaptivePortalEventStats(
161 CAPTIVE_PORTAL_PROBE_COMPLETED_OVERRIDABLE
:
162 CAPTIVE_PORTAL_PROBE_COMPLETED
);
163 // Log only one of portal detected and no response results.
164 if (captive_portal_detected_
)
165 RecordCaptivePortalEventStats(
167 CAPTIVE_PORTAL_DETECTED_OVERRIDABLE
:
168 CAPTIVE_PORTAL_DETECTED
);
169 else if (captive_portal_no_response_
)
170 RecordCaptivePortalEventStats(
172 CAPTIVE_PORTAL_NO_RESPONSE_OVERRIDABLE
:
173 CAPTIVE_PORTAL_NO_RESPONSE
);
177 void SSLErrorClassification::RecordUMAStatistics(
178 bool overridable
) const {
179 SSLErrorInfo::ErrorType type
=
180 SSLErrorInfo::NetErrorToErrorType(cert_error_
);
181 UMA_HISTOGRAM_ENUMERATION(
182 "interstitial.ssl_error_type", type
, SSLErrorInfo::END_OF_ENUM
);
184 case SSLErrorInfo::CERT_DATE_INVALID
: {
185 if (IsUserClockInThePast(base::Time::NowFromSystemTime())) {
186 RecordSSLInterstitialCause(overridable
, CLOCK_PAST
);
187 } else if (IsUserClockInTheFuture(base::Time::NowFromSystemTime())) {
188 RecordSSLInterstitialCause(overridable
, CLOCK_FUTURE
);
189 } else if (cert_
.HasExpired() && TimePassedSinceExpiry().InDays() < 28) {
190 RecordSSLInterstitialCause(overridable
, EXPIRED_RECENTLY
);
194 case SSLErrorInfo::CERT_COMMON_NAME_INVALID
: {
195 std::string host_name
= request_url_
.host();
196 if (IsHostNameKnownTLD(host_name
)) {
197 Tokens host_name_tokens
= Tokenize(host_name
);
198 if (IsWWWSubDomainMatch())
199 RecordSSLInterstitialCause(overridable
, WWW_SUBDOMAIN_MATCH
);
200 if (IsSubDomainOutsideWildcard(host_name_tokens
))
201 RecordSSLInterstitialCause(overridable
, SUBDOMAIN_OUTSIDE_WILDCARD
);
202 std::vector
<std::string
> dns_names
;
203 cert_
.GetDNSNames(&dns_names
);
204 std::vector
<Tokens
> dns_name_tokens
= GetTokenizedDNSNames(dns_names
);
205 if (NameUnderAnyNames(host_name_tokens
, dns_name_tokens
))
206 RecordSSLInterstitialCause(overridable
, SUBDOMAIN_MATCH
);
207 if (AnyNamesUnderName(dns_name_tokens
, host_name_tokens
))
208 RecordSSLInterstitialCause(overridable
, SUBDOMAIN_INVERSE_MATCH
);
209 if (IsCertLikelyFromMultiTenantHosting())
210 RecordSSLInterstitialCause(overridable
, LIKELY_MULTI_TENANT_HOSTING
);
212 RecordSSLInterstitialCause(overridable
, HOST_NAME_NOT_KNOWN_TLD
);
216 case SSLErrorInfo::CERT_AUTHORITY_INVALID
: {
217 const std::string
& hostname
= request_url_
.HostNoBrackets();
218 if (net::IsLocalhost(hostname
))
219 RecordSSLInterstitialCause(overridable
, LOCALHOST
);
220 if (IsHostnameNonUniqueOrDotless(hostname
))
221 RecordSSLInterstitialCause(overridable
, PRIVATE_URL
);
222 if (captive_portal_probe_completed_
&& captive_portal_detected_
)
223 RecordSSLInterstitialCause(overridable
, AUTHORITY_ERROR_CAPTIVE_PORTAL
);
224 if (net::X509Certificate::IsSelfSigned(cert_
.os_cert_handle()))
225 RecordSSLInterstitialCause(overridable
, SELF_SIGNED
);
231 UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.connection_type",
232 net::NetworkChangeNotifier::GetConnectionType(),
233 net::NetworkChangeNotifier::CONNECTION_LAST
);
236 base::TimeDelta
SSLErrorClassification::TimePassedSinceExpiry() const {
237 base::TimeDelta delta
= current_time_
- cert_
.valid_expiry();
241 bool SSLErrorClassification::IsUserClockInThePast(const base::Time
& time_now
) {
242 #if defined(DONT_EMBED_BUILD_METADATA) && !defined(OFFICIAL_BUILD)
245 base::Time build_time
= base::GetBuildTime();
246 if (time_now
< build_time
- base::TimeDelta::FromDays(2))
252 bool SSLErrorClassification::IsUserClockInTheFuture(
253 const base::Time
& time_now
) {
254 #if defined(DONT_EMBED_BUILD_METADATA) && !defined(OFFICIAL_BUILD)
257 base::Time build_time
= base::GetBuildTime();
258 if (time_now
> build_time
+ base::TimeDelta::FromDays(365))
264 bool SSLErrorClassification::MaybeWindowsLacksSHA256Support() {
266 return !base::win::MaybeHasSHA256Support();
272 bool SSLErrorClassification::IsHostNameKnownTLD(const std::string
& host_name
) {
274 net::registry_controlled_domains::GetRegistryLength(
276 net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES
,
277 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES
);
278 if (tld_length
== 0 || tld_length
== std::string::npos
)
283 std::vector
<SSLErrorClassification::Tokens
> SSLErrorClassification::
284 GetTokenizedDNSNames(const std::vector
<std::string
>& dns_names
) {
285 std::vector
<std::vector
<std::string
>> dns_name_tokens
;
286 for (size_t i
= 0; i
< dns_names
.size(); ++i
) {
287 std::vector
<std::string
> dns_name_token_single
;
288 if (dns_names
[i
].empty() || dns_names
[i
].find('\0') != std::string::npos
289 || !(IsHostNameKnownTLD(dns_names
[i
]))) {
290 dns_name_token_single
.push_back(std::string());
292 dns_name_token_single
= Tokenize(dns_names
[i
]);
294 dns_name_tokens
.push_back(dns_name_token_single
);
296 return dns_name_tokens
;
299 size_t SSLErrorClassification::FindSubDomainDifference(
300 const Tokens
& potential_subdomain
, const Tokens
& parent
) const {
301 // A check to ensure that the number of tokens in the tokenized_parent is
302 // less than the tokenized_potential_subdomain.
303 if (parent
.size() >= potential_subdomain
.size())
306 size_t tokens_match
= 0;
307 size_t diff_size
= potential_subdomain
.size() - parent
.size();
308 for (size_t i
= 0; i
< parent
.size(); ++i
) {
309 if (parent
[i
] == potential_subdomain
[i
+ diff_size
])
312 if (tokens_match
== parent
.size())
317 SSLErrorClassification::Tokens
SSLErrorClassification::
318 Tokenize(const std::string
& name
) {
320 base::SplitStringDontTrim(name
, '.', &name_tokens
);
324 // We accept the inverse case for www for historical reasons.
325 bool SSLErrorClassification::IsWWWSubDomainMatch() const {
326 std::string host_name
= request_url_
.host();
327 if (IsHostNameKnownTLD(host_name
)) {
328 std::vector
<std::string
> dns_names
;
329 cert_
.GetDNSNames(&dns_names
);
331 // Need to account for all possible domains given in the SSL certificate.
332 for (size_t i
= 0; i
< dns_names
.size(); ++i
) {
333 if (dns_names
[i
].empty() || dns_names
[i
].find('\0') != std::string::npos
334 || dns_names
[i
].length() == host_name
.length()
335 || !(IsHostNameKnownTLD(dns_names
[i
]))) {
336 result
= result
|| false;
337 } else if (dns_names
[i
].length() > host_name
.length()) {
339 net::StripWWW(base::ASCIIToUTF16(dns_names
[i
])) ==
340 base::ASCIIToUTF16(host_name
);
343 net::StripWWW(base::ASCIIToUTF16(host_name
)) ==
344 base::ASCIIToUTF16(dns_names
[i
]);
352 bool SSLErrorClassification::NameUnderAnyNames(
354 const std::vector
<Tokens
>& potential_parents
) const {
356 // Need to account for all the possible domains given in the SSL certificate.
357 for (size_t i
= 0; i
< potential_parents
.size(); ++i
) {
358 if (potential_parents
[i
].empty() ||
359 potential_parents
[i
].size() >= child
.size()) {
360 result
= result
|| false;
362 size_t domain_diff
= FindSubDomainDifference(child
,
363 potential_parents
[i
]);
364 if (domain_diff
== 1 && child
[0] != "www")
365 result
= result
|| true;
371 bool SSLErrorClassification::AnyNamesUnderName(
372 const std::vector
<Tokens
>& potential_children
,
373 const Tokens
& parent
) const {
375 // Need to account for all the possible domains given in the SSL certificate.
376 for (size_t i
= 0; i
< potential_children
.size(); ++i
) {
377 if (potential_children
[i
].empty() ||
378 potential_children
[i
].size() <= parent
.size()) {
379 result
= result
|| false;
381 size_t domain_diff
= FindSubDomainDifference(potential_children
[i
],
383 if (domain_diff
== 1 && potential_children
[i
][0] != "www")
384 result
= result
|| true;
390 bool SSLErrorClassification::IsSubDomainOutsideWildcard(
391 const Tokens
& host_name_tokens
) const {
392 std::string host_name
= request_url_
.host();
393 std::vector
<std::string
> dns_names
;
394 cert_
.GetDNSNames(&dns_names
);
397 // This method requires that the host name be longer than the dns name on
399 for (size_t i
= 0; i
< dns_names
.size(); ++i
) {
400 const std::string
& name
= dns_names
[i
];
401 if (name
.length() < 2 || name
.length() >= host_name
.length() ||
402 name
.find('\0') != std::string::npos
||
403 !IsHostNameKnownTLD(name
)
404 || name
[0] != '*' || name
[1] != '.') {
408 // Move past the "*.".
409 std::string extracted_dns_name
= name
.substr(2);
410 if (FindSubDomainDifference(
411 host_name_tokens
, Tokenize(extracted_dns_name
)) == 2) {
418 bool SSLErrorClassification::IsCertLikelyFromMultiTenantHosting() const {
419 std::string host_name
= request_url_
.host();
420 std::vector
<std::string
> dns_names
;
421 std::vector
<std::string
> dns_names_domain
;
422 cert_
.GetDNSNames(&dns_names
);
423 size_t dns_names_size
= dns_names
.size();
425 // If there is only 1 DNS name then it is definitely not a shared certificate.
426 if (dns_names_size
== 0 || dns_names_size
== 1)
429 // Check to see if all the domains in the SAN field in the SSL certificate are
431 for (size_t i
= 0; i
< dns_names_size
; ++i
) {
432 dns_names_domain
.push_back(
433 net::registry_controlled_domains::
434 GetDomainAndRegistry(
436 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES
));
438 for (size_t i
= 1; i
< dns_names_domain
.size(); ++i
) {
439 if (dns_names_domain
[i
] != dns_names_domain
[0])
443 // If the number of DNS names is more than 5 then assume that it is a shared
445 static const int kDistinctNameThreshold
= 5;
446 if (dns_names_size
> kDistinctNameThreshold
)
449 // Heuristic - The edit distance between all the strings should be at least 5
450 // for it to be counted as a shared SSLCertificate. If even one pair of
451 // strings edit distance is below 5 then the certificate is no longer
452 // considered as a shared certificate. Include the host name in the URL also
454 dns_names
.push_back(host_name
);
455 static const int kMinimumEditDsitance
= 5;
456 for (size_t i
= 0; i
< dns_names_size
; ++i
) {
457 for (size_t j
= i
+ 1; j
< dns_names_size
; ++j
) {
458 int edit_distance
= GetLevensteinDistance(dns_names
[i
], dns_names
[j
]);
459 if (edit_distance
< kMinimumEditDsitance
)
467 bool SSLErrorClassification::IsHostnameNonUniqueOrDotless(
468 const std::string
& hostname
) {
469 return net::IsHostnameNonUnique(hostname
) ||
470 hostname
.find('.') == std::string::npos
;
473 void SSLErrorClassification::Observe(
475 const content::NotificationSource
& source
,
476 const content::NotificationDetails
& details
) {
477 #if defined(ENABLE_CAPTIVE_PORTAL_DETECTION)
478 // When detection is disabled, captive portal service always sends
479 // RESULT_INTERNET_CONNECTED. Ignore any probe results in that case.
480 if (!captive_portal_detection_enabled_
)
482 if (type
== chrome::NOTIFICATION_CAPTIVE_PORTAL_CHECK_RESULT
) {
483 captive_portal_probe_completed_
= true;
484 CaptivePortalService::Results
* results
=
485 content::Details
<CaptivePortalService::Results
>(details
).ptr();
486 // If a captive portal was detected at any point when the interstitial was
487 // displayed, assume that the interstitial was caused by a captive portal.
489 // 1- Interstitial displayed and captive portal detected, setting the flag.
490 // 2- Captive portal detection automatically opens portal login page.
491 // 3- User logs in on the portal login page.
492 // A notification will be received here for RESULT_INTERNET_CONNECTED. Make
493 // sure we don't clear the captive protal flag, since the interstitial was
494 // potentially caused by the captive portal.
495 captive_portal_detected_
= captive_portal_detected_
||
496 (results
->result
== captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL
);
497 // Also keep track of non-HTTP portals and error cases.
498 captive_portal_no_response_
= captive_portal_no_response_
||
499 (results
->result
== captive_portal::RESULT_NO_RESPONSE
);