Add initial version of captive portal list checking.
This CL adds captive portal certificate list checking feature. When an SSL
error occurs, the feature checks the certificate chain's SPKI hashes to a
list of hashes that are known to be served by captive portals. The list is
embedded as a resource and currently only contains a single hash (the hash
of the leaf cert of captive-portal.badssl.com). Follow up CLs will introduce
a component updater component to dynamically update the list of known captive
portal SPKI hashes.
BUG=640835
CQ_INCLUDE_TRYBOTS=master.tryserver.chromium.linux:closure_compilation
Review-Url: https://codereview.chromium.org/2620203003
Cr-Commit-Position: refs/heads/master@{#448796}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 9b896b7..e8fbac0b 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -1378,6 +1378,7 @@
"//chrome/browser/metrics/variations:chrome_ui_string_overrider_factory",
"//chrome/browser/net:probe_message_proto",
"//chrome/browser/resources:component_extension_resources",
+ "//chrome/browser/ssl:proto",
"//chrome/browser/ui",
"//chrome/common/net",
"//chrome/installer/util:with_no_strings",
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 576408b..c6d40d4 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -640,6 +640,9 @@
<include name="IDR_WELCOME_WIN10_PIN_LARGE_WEBP" file="resources\welcome\win10\pin-large.webp" type="BINDATA" />
<include name="IDR_WELCOME_WIN10_PIN_SMALL_WEBP" file="resources\welcome\win10\pin-small.webp" type="BINDATA" />
</if>
+ <if expr="not is_android and not is_ios">
+ <include name="IDR_SSL_ERROR_ASSISTANT_PB" file="${root_gen_dir}/chrome/browser/resources/ssl/ssl_error_assistant/ssl_error_assistant.pb" use_base_dir="false" type="BINDATA" />
+ </if>
</includes>
</release>
</grit>
diff --git a/chrome/browser/resources/ssl/ssl_error_assistant/ssl_error_assistant.asciipb b/chrome/browser/resources/ssl/ssl_error_assistant/ssl_error_assistant.asciipb
index a6714bf5..64755fd 100644
--- a/chrome/browser/resources/ssl/ssl_error_assistant/ssl_error_assistant.asciipb
+++ b/chrome/browser/resources/ssl/ssl_error_assistant/ssl_error_assistant.asciipb
@@ -2,17 +2,14 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
-# Placeholder for list of known certificates that are treated as captive
-# portal certificates.
+# Placeholder list for SPKI hashes of certificates that are treated as captive
+# portal certificates. See chrome/browser/ssl/ssl_error_assistant.proto for the
+# full format.
# TODO(crbug.com/640835): Populate this with full list.
version_id: 1
-# See chrome/browser/ssl/tls_error_assistant.proto for the full format.
+# https://captive-portal.badssl.com leaf:
captive_portal_cert {
-
- # Sha256 hash of the certificate's public key. The fingerprint is prefixed
- # with "sha256/" and encoded in base64 (same format as
- # src/net/http/transport_security_state_static.pins)
- sha256_hash: "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
+ sha256_hash: "sha256/fjZPHewEHTrMDX3I1ecEIeoy3WFxHyGplOLv28kIbtI="
}
diff --git a/chrome/browser/ssl/ssl_browser_tests.cc b/chrome/browser/ssl/ssl_browser_tests.cc
index 9bbf09a..4af5ff95 100644
--- a/chrome/browser/ssl/ssl_browser_tests.cc
+++ b/chrome/browser/ssl/ssl_browser_tests.cc
@@ -18,6 +18,7 @@
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/default_clock.h"
@@ -38,6 +39,7 @@
#include "chrome/browser/ssl/common_name_mismatch_handler.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "chrome/browser/ssl/ssl_blocking_page.h"
+#include "chrome/browser/ssl/ssl_error_assistant.pb.h"
#include "chrome/browser/ssl/ssl_error_handler.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
@@ -84,9 +86,11 @@
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
+#include "crypto/sha2.h"
#include "net/base/host_port_pair.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
+#include "net/cert/asn1_util.h"
#include "net/cert/cert_status_flags.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/cert/x509_certificate.h"
@@ -104,6 +108,10 @@
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_test_util.h"
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+#include "chrome/browser/ssl/captive_portal_blocking_page.h"
+#endif
+
#if defined(USE_NSS_CERTS)
#include "chrome/browser/net/nss_context.h"
#include "net/base/crypto_module.h"
@@ -226,12 +234,17 @@
// Waits until the interstitial delay timer in SSLErrorHandler is started.
void WaitForTimerStarted() { message_loop_runner_->Run(); }
+ // Returns true if the interstitial delay timer has been started.
+ bool timer_started() const { return timer_started_; }
+
private:
void OnTimerStarted(content::WebContents* web_contents) {
+ timer_started_ = true;
if (web_contents_ == web_contents)
message_loop_runner_->Quit();
}
+ bool timer_started_ = false;
const content::WebContents* web_contents_;
SSLErrorHandler::TimerStartedCallback callback_;
@@ -273,6 +286,19 @@
return std::string(buffer.data(), buffer.length());
}
+// Returns the Sha256 hash of the SPKI of |cert|.
+net::HashValue GetSPKIHash(net::X509Certificate* cert) {
+ std::string der_data;
+ EXPECT_TRUE(
+ net::X509Certificate::GetDEREncoded(cert->os_cert_handle(), &der_data));
+ base::StringPiece der_bytes(der_data);
+ base::StringPiece spki_bytes;
+ EXPECT_TRUE(net::asn1::ExtractSPKIFromDERCert(der_bytes, &spki_bytes));
+ net::HashValue sha256(net::HASH_VALUE_SHA256);
+ crypto::SHA256HashString(spki_bytes, sha256.data(), crypto::kSHA256Length);
+ return sha256;
+}
+
} // namespace
class SSLUITest : public InProcessBrowserTest {
@@ -308,6 +334,16 @@
"https", "localhost", std::move(interceptor));
}
+ void SetUp() override {
+ InProcessBrowserTest::SetUp();
+ SSLErrorHandler::ResetConfigForTesting();
+ }
+
+ void TearDown() override {
+ SSLErrorHandler::ResetConfigForTesting();
+ InProcessBrowserTest::TearDown();
+ }
+
void SetUpCommandLine(base::CommandLine* command_line) override {
// Browser will both run and display insecure content.
command_line->AppendSwitch(switches::kAllowRunningInsecureContent);
@@ -3885,6 +3921,290 @@
ASSERT_TRUE(content::ExecuteScript(tab, "window.open()"));
}
+// Put captive portal related tests under a different namespace for nicer
+// pattern matching.
+using SSLUICaptivePortalListTest = SSLUITest;
+
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+
+// Tests that the captive portal certificate list is not used when the feature
+// is disabled via Finch. The list is passed to SSLErrorHandler via a proto.
+IN_PROC_BROWSER_TEST_F(SSLUICaptivePortalListTest,
+ CaptivePortalCertificateList_Disabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ // Use InitFromCommandLine instead of InitAndDisableFeature to avoid making
+ // the feature public in SSLErrorHandler header.
+ scoped_feature_list.InitFromCommandLine(
+ std::string() /* enabled */,
+ "CaptivePortalCertificateList" /* disabled */);
+
+ ASSERT_TRUE(https_server_mismatched_.Start());
+ base::HistogramTester histograms;
+
+ // Mark the server's cert as a captive portal cert.
+ const net::HashValue server_spki_hash =
+ GetSPKIHash(https_server_mismatched_.GetCertificate().get());
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ server_spki_hash.ToString());
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ // Navigate to an unsafe page on the server. A normal SSL interstitial should
+ // be displayed since CaptivePortalCertificateList feature is disabled.
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ SSLInterstitialTimerObserver interstitial_timer_observer(tab);
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_mismatched_.GetURL("/ssl/blank_page.html"));
+ content::WaitForInterstitialAttach(tab);
+
+ InterstitialPage* interstitial_page = tab->GetInterstitialPage();
+ ASSERT_EQ(SSLBlockingPage::kTypeForTesting,
+ interstitial_page->GetDelegateForTesting()->GetTypeForTesting());
+ EXPECT_TRUE(interstitial_timer_observer.timer_started());
+
+ // Check that the histogram for the SSL interstitial was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 2);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_SSL_INTERSTITIAL_OVERRIDABLE, 1);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::CAPTIVE_PORTAL_CERT_FOUND, 0);
+}
+
+// Tests that the captive portal certificate list is used when the feature
+// is enabled via Finch. The list is passed to SSLErrorHandler via a proto.
+IN_PROC_BROWSER_TEST_F(SSLUICaptivePortalListTest,
+ CaptivePortalCertificateList_Enabled_FromProto) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */,
+ std::string() /* disabled */);
+
+ ASSERT_TRUE(https_server_mismatched_.Start());
+ base::HistogramTester histograms;
+
+ // Mark the server's cert as a captive portal cert.
+ const net::HashValue server_spki_hash =
+ GetSPKIHash(https_server_mismatched_.GetCertificate().get());
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ server_spki_hash.ToString());
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ // Navigate to an unsafe page on the server. The captive portal interstitial
+ // should be displayed since CaptivePortalCertificateList feature is enabled.
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ SSLInterstitialTimerObserver interstitial_timer_observer(tab);
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_mismatched_.GetURL("/ssl/blank_page.html"));
+ content::WaitForInterstitialAttach(tab);
+
+ InterstitialPage* interstitial_page = tab->GetInterstitialPage();
+ ASSERT_EQ(CaptivePortalBlockingPage::kTypeForTesting,
+ interstitial_page->GetDelegateForTesting()->GetTypeForTesting());
+ EXPECT_FALSE(interstitial_timer_observer.timer_started());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 3);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE, 1);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::CAPTIVE_PORTAL_CERT_FOUND, 1);
+}
+
+namespace {
+
+// Test class that mimics a URL request with a certificate whose SPKI hash is in
+// ssl_error_assistant.asciipb resource. A better way of testing the SPKI hashes
+// inside the resource bundle would be to serve the actual certificate from the
+// embedded test server, but the test server can only serve a limited number of
+// predefined certificates.
+class SSLUICaptivePortalListResourceBundleTest
+ : public CertVerifierBrowserTest {
+ public:
+ SSLUICaptivePortalListResourceBundleTest()
+ : CertVerifierBrowserTest(),
+ https_server_(net::EmbeddedTestServer::TYPE_HTTPS),
+ https_server_mismatched_(net::EmbeddedTestServer::TYPE_HTTPS) {
+ https_server_.ServeFilesFromSourceDirectory(base::FilePath(kDocRoot));
+
+ https_server_mismatched_.SetSSLConfig(
+ net::EmbeddedTestServer::CERT_MISMATCHED_NAME);
+ https_server_mismatched_.AddDefaultHandlers(base::FilePath(kDocRoot));
+ }
+
+ void SetUp() override {
+ CertVerifierBrowserTest::SetUp();
+ SSLErrorHandler::ResetConfigForTesting();
+ SetUpCertVerifier(0, net::OK);
+ }
+
+ void TearDown() override {
+ SSLErrorHandler::ResetConfigForTesting();
+ CertVerifierBrowserTest::TearDown();
+ }
+
+ protected:
+ void SetUpCertVerifier(net::CertStatus cert_status, int net_result) {
+ scoped_refptr<net::X509Certificate> cert(https_server_.GetCertificate());
+ net::CertVerifyResult verify_result;
+ verify_result.is_issued_by_known_root =
+ (net_result != net::ERR_CERT_AUTHORITY_INVALID);
+ verify_result.verified_cert = cert;
+ verify_result.cert_status = cert_status;
+
+ // Set the SPKI hash to captive-portal.badssl.com leaf certificate. This
+ // doesn't match the actual cert (ok_cert.pem) but is good enough for
+ // testing.
+ net::HashValue hash;
+ ASSERT_TRUE(
+ hash.FromString("sha256/fjZPHewEHTrMDX3I1ecEIeoy3WFxHyGplOLv28kIbtI="));
+ verify_result.public_key_hashes.push_back(hash);
+ mock_cert_verifier()->AddResultForCert(cert, verify_result, net_result);
+ }
+
+ net::EmbeddedTestServer* https_server() { return &https_server_; }
+ net::EmbeddedTestServer* https_server_mismatched() {
+ return &https_server_mismatched_;
+ }
+
+ private:
+ net::EmbeddedTestServer https_server_;
+ net::EmbeddedTestServer https_server_mismatched_;
+};
+
+} // namespace
+
+// Same as CaptivePortalCertificateList_Enabled_FromProto, but this time the
+// cert's SPKI hash is listed in ssl_error_assistant.asciipb.
+IN_PROC_BROWSER_TEST_F(SSLUICaptivePortalListResourceBundleTest,
+ Enabled_FromResource) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */,
+ std::string() /* disabled */);
+ ASSERT_TRUE(https_server()->Start());
+ base::HistogramTester histograms;
+
+ // Mark the server's cert as a captive portal cert.
+ SetUpCertVerifier(net::CERT_STATUS_COMMON_NAME_INVALID,
+ net::ERR_CERT_COMMON_NAME_INVALID);
+
+ // Navigate to an unsafe page on the server. The captive portal interstitial
+ // should be displayed since CaptivePortalCertificateList feature is enabled.
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ SSLInterstitialTimerObserver interstitial_timer_observer(tab);
+ ui_test_utils::NavigateToURL(browser(), https_server()->GetURL("/"));
+ content::WaitForInterstitialAttach(tab);
+
+ InterstitialPage* interstitial_page = tab->GetInterstitialPage();
+ ASSERT_EQ(CaptivePortalBlockingPage::kTypeForTesting,
+ interstitial_page->GetDelegateForTesting()->GetTypeForTesting());
+ EXPECT_FALSE(interstitial_timer_observer.timer_started());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 3);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE, 1);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::CAPTIVE_PORTAL_CERT_FOUND, 1);
+}
+
+// Same as SSLUICaptivePortalNameMismatchTest, but this time the error is
+// authority-invalid. Captive portal interstitial should not be shown.
+IN_PROC_BROWSER_TEST_F(SSLUICaptivePortalListResourceBundleTest,
+ Enabled_FromResource_AuthorityInvalid) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */,
+ std::string() /* disabled */);
+ ASSERT_TRUE(https_server()->Start());
+ base::HistogramTester histograms;
+
+ // Set interstitial delay to zero.
+ SSLErrorHandler::SetInterstitialDelayForTesting(base::TimeDelta());
+ // Mark the server's cert as a captive portal cert, but with an
+ // authority-invalid error.
+ SetUpCertVerifier(net::CERT_STATUS_AUTHORITY_INVALID,
+ net::ERR_CERT_AUTHORITY_INVALID);
+
+ // Navigate to an unsafe page on the server. CaptivePortalCertificateList
+ // feature is enabled but the error is not a name mismatch, so a generic SSL
+ // interstitial should be displayed.
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ SSLInterstitialTimerObserver interstitial_timer_observer(tab);
+ ui_test_utils::NavigateToURL(browser(), https_server()->GetURL("/"));
+ content::WaitForInterstitialAttach(tab);
+
+ InterstitialPage* interstitial_page = tab->GetInterstitialPage();
+ ASSERT_EQ(SSLBlockingPage::kTypeForTesting,
+ interstitial_page->GetDelegateForTesting()->GetTypeForTesting());
+ EXPECT_TRUE(interstitial_timer_observer.timer_started());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 2);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_SSL_INTERSTITIAL_OVERRIDABLE, 1);
+}
+
+#else
+
+// Tests that the captive portal certificate list is not used when captive
+// portal checks are disabled by build, even if the captive portal certificate
+// list feature is enabled via Finch. The list is passed to SSLErrorHandler via
+// a proto.
+IN_PROC_BROWSER_TEST_F(SSLUICaptivePortalListTest, PortalChecksDisabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */,
+ std::string() /* disabled */);
+
+ ASSERT_TRUE(https_server_mismatched_.Start());
+ base::HistogramTester histograms;
+
+ // Mark the server's cert as a captive portal cert.
+ const net::HashValue server_spki_hash =
+ GetSPKIHash(https_server_mismatched_.GetCertificate().get());
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ server_spki_hash.ToString());
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ // Navigate to an unsafe page on the server. The captive portal interstitial
+ // should be displayed since CaptivePortalCertificateList feature is enabled.
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ SSLInterstitialTimerObserver interstitial_timer_observer(tab);
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_mismatched_.GetURL("/ssl/blank_page.html"));
+ content::WaitForInterstitialAttach(tab);
+
+ InterstitialPage* interstitial_page = tab->GetInterstitialPage();
+ ASSERT_EQ(SSLBlockingPage::kTypeForTesting,
+ interstitial_page->GetDelegateForTesting()->GetTypeForTesting());
+ EXPECT_FALSE(interstitial_timer_observer.timer_started());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 2);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_SSL_INTERSTITIAL_OVERRIDABLE, 1);
+}
+
+#endif // BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+
// TODO(jcampan): more tests to do below.
// Visit a page over https that contains a frame with a redirect.
diff --git a/chrome/browser/ssl/ssl_error_assistant.proto b/chrome/browser/ssl/ssl_error_assistant.proto
index ca64528e..ba05957c 100644
--- a/chrome/browser/ssl/ssl_error_assistant.proto
+++ b/chrome/browser/ssl/ssl_error_assistant.proto
@@ -13,6 +13,8 @@
// with "sha256/" and encoded in base64 (same format as
// src/net/http/transport_security_state_static.pins)
// Example: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
+ //
+ // NOTE: Only leaf certs must be added here.
optional string sha256_hash = 1;
}
diff --git a/chrome/browser/ssl/ssl_error_handler.cc b/chrome/browser/ssl/ssl_error_handler.cc
index 8ddc06f..5cbf4be 100644
--- a/chrome/browser/ssl/ssl_error_handler.cc
+++ b/chrome/browser/ssl/ssl_error_handler.cc
@@ -5,12 +5,15 @@
#include "chrome/browser/ssl/ssl_error_handler.h"
#include <stdint.h>
+#include <unordered_set>
#include <utility>
#include "base/callback_helpers.h"
#include "base/feature_list.h"
+#include "base/files/file_util.h"
#include "base/lazy_instance.h"
#include "base/macros.h"
+#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/threading/non_thread_safe.h"
@@ -21,22 +24,27 @@
#include "chrome/browser/ssl/bad_clock_blocking_page.h"
#include "chrome/browser/ssl/ssl_blocking_page.h"
#include "chrome/browser/ssl/ssl_cert_reporter.h"
+#include "chrome/browser/ssl/ssl_error_assistant.pb.h"
#include "chrome/common/features.h"
+#include "chrome/grit/browser_resources.h"
#include "components/network_time/network_time_tracker.h"
#include "components/ssl_errors/error_classification.h"
#include "components/ssl_errors/error_info.h"
+#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/net_errors.h"
+#include "ui/base/resource/resource_bundle.h"
#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
#include "chrome/browser/captive_portal/captive_portal_service.h"
#include "chrome/browser/captive_portal/captive_portal_service_factory.h"
#include "chrome/browser/captive_portal/captive_portal_tab_helper.h"
#include "chrome/browser/ssl/captive_portal_blocking_page.h"
+#include "third_party/protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h"
#endif
namespace {
@@ -44,13 +52,14 @@
#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
const base::Feature kCaptivePortalInterstitial{
"CaptivePortalInterstitial", base::FEATURE_ENABLED_BY_DEFAULT};
+
+const base::Feature kCaptivePortalCertificateList{
+ "CaptivePortalCertificateList", base::FEATURE_DISABLED_BY_DEFAULT};
#endif
const base::Feature kSSLCommonNameMismatchHandling{
"SSLCommonNameMismatchHandling", base::FEATURE_ENABLED_BY_DEFAULT};
-const char kHistogram[] = "interstitial.ssl_error_handler";
-
// Default delay in milliseconds before displaying the SSL interstitial.
// This can be changed in tests.
// - If there is a name mismatch and a suggested URL available result arrives
@@ -60,6 +69,8 @@
// - Otherwise, an SSL interstitial is displayed.
const int64_t kInterstitialDelayInMilliseconds = 3000;
+const char kHistogram[] = "interstitial.ssl_error_handler";
+
// Adds a message to console after navigation commits and then, deletes itself.
// Also deletes itself if the navigation is stopped.
class CommonNameMismatchRedirectObserver
@@ -126,6 +137,28 @@
bool IsCaptivePortalInterstitialEnabled() {
return base::FeatureList::IsEnabled(kCaptivePortalInterstitial);
}
+
+// Reads the SSL error assistant configuration from the resource bundle.
+bool ReadErrorAssistantProtoFromResourceBundle(
+ chrome_browser_ssl::SSLErrorAssistantConfig* proto) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ DCHECK(proto);
+ ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
+ base::StringPiece data =
+ bundle.GetRawDataResource(IDR_SSL_ERROR_ASSISTANT_PB);
+ google::protobuf::io::ArrayInputStream stream(data.data(), data.size());
+ return proto->ParseFromZeroCopyStream(&stream);
+}
+
+std::unique_ptr<std::unordered_set<std::string>> LoadCaptivePortalCertHashes(
+ const chrome_browser_ssl::SSLErrorAssistantConfig& proto) {
+ auto hashes = base::MakeUnique<std::unordered_set<std::string>>();
+ for (const chrome_browser_ssl::CaptivePortalCert& cert :
+ proto.captive_portal_cert()) {
+ hashes.get()->insert(cert.sha256_hash());
+ }
+ return hashes;
+}
#endif
bool IsSSLCommonNameMismatchHandlingEnabled() {
@@ -142,6 +175,15 @@
base::Clock* clock() const;
network_time::NetworkTimeTracker* network_time_tracker() const;
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ // Returns true if any of the SHA256 hashes in |ssl_info| is of a captive
+ // portal certificate. The set of captive portal hashes is loaded on first
+ // use.
+ bool IsKnownCaptivePortalCert(const net::SSLInfo& ssl_info);
+#endif
+
+ // Testing methods:
+ void ResetForTesting();
void SetInterstitialDelayForTesting(const base::TimeDelta& delay);
void SetTimerStartedCallbackForTesting(
SSLErrorHandler::TimerStartedCallback* callback);
@@ -149,6 +191,11 @@
void SetNetworkTimeTrackerForTesting(
network_time::NetworkTimeTracker* tracker);
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ void SetErrorAssistantProtoForTesting(
+ const chrome_browser_ssl::SSLErrorAssistantConfig& error_assistant_proto);
+#endif
+
private:
base::TimeDelta interstitial_delay_;
@@ -161,6 +208,13 @@
base::Clock* testing_clock_ = nullptr;
network_time::NetworkTimeTracker* network_time_tracker_ = nullptr;
+
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ // SPKI hashes belonging to certs treated as captive portals. Null until the
+ // first time IsKnownCaptivePortalCert() or SetErrorAssistantProtoForTesting()
+ // is called.
+ std::unique_ptr<std::unordered_set<std::string>> captive_portal_spki_hashes_;
+#endif
};
ConfigSingleton::ConfigSingleton()
@@ -187,6 +241,17 @@
return testing_clock_;
}
+void ConfigSingleton::ResetForTesting() {
+ interstitial_delay_ =
+ base::TimeDelta::FromMilliseconds(kInterstitialDelayInMilliseconds);
+ timer_started_callback_ = nullptr;
+ network_time_tracker_ = nullptr;
+ testing_clock_ = nullptr;
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ captive_portal_spki_hashes_.reset();
+#endif
+}
+
void ConfigSingleton::SetInterstitialDelayForTesting(
const base::TimeDelta& delay) {
interstitial_delay_ = delay;
@@ -207,6 +272,36 @@
network_time_tracker_ = tracker;
}
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+void ConfigSingleton::SetErrorAssistantProtoForTesting(
+ const chrome_browser_ssl::SSLErrorAssistantConfig& error_assistant_proto) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ DCHECK(!captive_portal_spki_hashes_);
+ captive_portal_spki_hashes_ =
+ LoadCaptivePortalCertHashes(error_assistant_proto);
+}
+
+bool ConfigSingleton::IsKnownCaptivePortalCert(const net::SSLInfo& ssl_info) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ if (!captive_portal_spki_hashes_) {
+ chrome_browser_ssl::SSLErrorAssistantConfig proto;
+ CHECK(ReadErrorAssistantProtoFromResourceBundle(&proto));
+ captive_portal_spki_hashes_ = LoadCaptivePortalCertHashes(proto);
+ }
+
+ for (const net::HashValue& hash_value : ssl_info.public_key_hashes) {
+ if (hash_value.tag != net::HASH_VALUE_SHA256) {
+ continue;
+ }
+ if (captive_portal_spki_hashes_->find(hash_value.ToString()) !=
+ captive_portal_spki_hashes_->end()) {
+ return true;
+ }
+ }
+ return false;
+}
+#endif
+
class SSLErrorHandlerDelegateImpl : public SSLErrorHandler::Delegate {
public:
SSLErrorHandlerDelegateImpl(
@@ -365,6 +460,11 @@
}
// static
+void SSLErrorHandler::ResetConfigForTesting() {
+ g_config.Pointer()->ResetForTesting();
+}
+
+// static
void SSLErrorHandler::SetInterstitialDelayForTesting(
const base::TimeDelta& delay) {
g_config.Pointer()->SetInterstitialDelayForTesting(delay);
@@ -396,6 +496,13 @@
return timer_.IsRunning();
}
+void SSLErrorHandler::SetErrorAssistantProtoForTesting(
+ const chrome_browser_ssl::SSLErrorAssistantConfig& config_proto) {
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ g_config.Pointer()->SetErrorAssistantProtoForTesting(config_proto);
+#endif
+}
+
SSLErrorHandler::SSLErrorHandler(
std::unique_ptr<Delegate> delegate,
content::WebContents* web_contents,
@@ -426,6 +533,17 @@
return;
}
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+ if (base::FeatureList::IsEnabled(kCaptivePortalCertificateList) &&
+ cert_error_ == net::ERR_CERT_COMMON_NAME_INVALID &&
+ g_config.Pointer()->IsKnownCaptivePortalCert(ssl_info_)) {
+ RecordUMA(CAPTIVE_PORTAL_CERT_FOUND);
+ ShowCaptivePortalInterstitial(
+ GURL(captive_portal::CaptivePortalDetector::kDefaultURL));
+ return;
+ }
+#endif
+
std::vector<std::string> dns_names;
ssl_info_.cert->GetDNSNames(&dns_names);
DCHECK(!dns_names.empty());
diff --git a/chrome/browser/ssl/ssl_error_handler.h b/chrome/browser/ssl/ssl_error_handler.h
index bdc443a..80089d737 100644
--- a/chrome/browser/ssl/ssl_error_handler.h
+++ b/chrome/browser/ssl/ssl_error_handler.h
@@ -15,6 +15,7 @@
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/common_name_mismatch_handler.h"
#include "chrome/browser/ssl/ssl_cert_reporter.h"
+#include "chrome/browser/ssl/ssl_error_assistant.pb.h"
#include "components/ssl_errors/error_classification.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_registrar.h"
@@ -40,16 +41,17 @@
class NetworkTimeTracker;
}
-// This class is responsible for deciding what type of interstitial to show for
-// an SSL validation error. The display of the interstitial might be delayed by
-// a few seconds while trying to determine the cause of the error. During this
-// window, the class will: check for a clock error, wait for a name-mismatch
-// suggested URL, or wait for a captive portal result to arrive. If there is a
-// name mismatch error and a corresponding suggested URL result arrives in this
-// window, the user is redirected to the suggested URL.
-// Failing that, if a captive portal detected result arrives in the time window,
-// a captive portal error page is shown. If none of these potential error
-// causes match, an SSL interstitial is shown.
+// This class is responsible for deciding what type of interstitial to display
+// for an SSL validation error and actually displaying it. The display of the
+// interstitial might be delayed by a few seconds while trying to determine the
+// cause of the error. During this window, the class will:
+// - Check for a clock error
+// - Check for a known captive portal certificate SPKI
+// - Wait for a name-mismatch suggested URL
+// - or Wait for a captive portal result to arrive.
+// Based on the result of these checks, SSLErrorHandler will show a customized
+// interstitial, redirect to a different suggested URL, or, if all else fails,
+// show the normal SSL interstitial.
//
// This class should only be used on the UI thread because its implementation
// uses captive_portal::CaptivePortalService which can only be accessed on the
@@ -72,6 +74,7 @@
WWW_MISMATCH_URL_AVAILABLE,
WWW_MISMATCH_URL_NOT_AVAILABLE,
SHOW_BAD_CLOCK,
+ CAPTIVE_PORTAL_CERT_FOUND,
SSL_ERROR_HANDLER_EVENT_COUNT
};
@@ -108,6 +111,7 @@
callback);
// Testing methods.
+ static void ResetConfigForTesting();
static void SetInterstitialDelayForTesting(const base::TimeDelta& delay);
// The callback pointer must remain valid for the duration of error handling.
static void SetInterstitialTimerStartedCallbackForTesting(
@@ -115,7 +119,12 @@
static void SetClockForTesting(base::Clock* testing_clock);
static void SetNetworkTimeTrackerForTesting(
network_time::NetworkTimeTracker* tracker);
+ static void SetErrorAssistantProtoForTesting(
+ const chrome_browser_ssl::SSLErrorAssistantConfig& config_proto);
static std::string GetHistogramNameForTesting();
+ static void SetErrorAssistantConfig(
+ std::unique_ptr<chrome_browser_ssl::SSLErrorAssistantConfig>
+ config_proto);
bool IsTimerRunningForTesting() const;
protected:
diff --git a/chrome/browser/ssl/ssl_error_handler_unittest.cc b/chrome/browser/ssl/ssl_error_handler_unittest.cc
index 680a98a..97e3793 100644
--- a/chrome/browser/ssl/ssl_error_handler_unittest.cc
+++ b/chrome/browser/ssl/ssl_error_handler_unittest.cc
@@ -10,12 +10,14 @@
#include "base/metrics/field_trial.h"
#include "base/run_loop.h"
#include "base/test/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/captive_portal/captive_portal_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/common_name_mismatch_handler.h"
+#include "chrome/browser/ssl/ssl_error_assistant.pb.h"
#include "chrome/common/features.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
@@ -43,6 +45,8 @@
const char kCertDateErrorHistogram[] =
"interstitial.ssl_error_handler.cert_date_error_delay";
+const net::SHA256HashValue kCertPublicKeyHashValue = {{0x01, 0x02}};
+
// Runs |quit_closure| on the UI thread once a URL request has been
// seen. Returns a request that hangs.
std::unique_ptr<net::test_server::HttpResponse> WaitForRequest(
@@ -187,16 +191,21 @@
} // namespace
-class SSLErrorHandlerNameMismatchTest : public ChromeRenderViewHostTestHarness {
+template <net::CertStatus cert_status>
+class SSLErrorHandlerCertStatusTestBase
+ : public ChromeRenderViewHostTestHarness {
public:
- SSLErrorHandlerNameMismatchTest() : field_trial_list_(nullptr) {}
+ SSLErrorHandlerCertStatusTestBase() : field_trial_list_(nullptr) {}
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
+ SSLErrorHandler::ResetConfigForTesting();
SSLErrorHandler::SetInterstitialDelayForTesting(base::TimeDelta());
ssl_info_.cert =
net::ImportCertFromFile(net::GetTestCertsDirectory(), "ok_cert.pem");
- ssl_info_.cert_status = net::CERT_STATUS_COMMON_NAME_INVALID;
+ ssl_info_.cert_status = cert_status;
+ ssl_info_.public_key_hashes.push_back(
+ net::HashValue(kCertPublicKeyHashValue));
delegate_ =
new TestSSLErrorHandlerDelegate(profile(), web_contents(), ssl_info_);
@@ -218,21 +227,29 @@
void TearDown() override {
EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
error_handler_.reset(nullptr);
+ SSLErrorHandler::ResetConfigForTesting();
ChromeRenderViewHostTestHarness::TearDown();
}
TestSSLErrorHandler* error_handler() { return error_handler_.get(); }
TestSSLErrorHandlerDelegate* delegate() { return delegate_; }
+ const net::SSLInfo& ssl_info() { return ssl_info_; }
+
private:
net::SSLInfo ssl_info_;
std::unique_ptr<TestSSLErrorHandler> error_handler_;
TestSSLErrorHandlerDelegate* delegate_;
base::FieldTrialList field_trial_list_;
- DISALLOW_COPY_AND_ASSIGN(SSLErrorHandlerNameMismatchTest);
+ DISALLOW_COPY_AND_ASSIGN(SSLErrorHandlerCertStatusTestBase);
};
+using SSLErrorHandlerNameMismatchTest =
+ SSLErrorHandlerCertStatusTestBase<net::CERT_STATUS_COMMON_NAME_INVALID>;
+using SSLErrorHandlerAuthorityInvalidTest =
+ SSLErrorHandlerCertStatusTestBase<net::CERT_STATUS_AUTHORITY_INVALID>;
+
class SSLErrorHandlerDateInvalidTest : public ChromeRenderViewHostTestHarness {
public:
SSLErrorHandlerDateInvalidTest()
@@ -246,6 +263,7 @@
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
+ SSLErrorHandler::ResetConfigForTesting();
field_trial_test()->SetNetworkQueriesWithVariationsService(
false, 0.0,
@@ -286,6 +304,7 @@
EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
error_handler_.reset(nullptr);
}
+ SSLErrorHandler::ResetConfigForTesting();
ChromeRenderViewHostTestHarness::TearDown();
}
@@ -679,3 +698,157 @@
// Shut down the server to cancel the pending request.
ASSERT_TRUE(test_server()->ShutdownAndWaitUntilComplete());
}
+
+#if BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)
+
+// Tests that a certificate marked as a known captive portal certificate causes
+// the captive portal interstitial to be shown.
+TEST_F(SSLErrorHandlerNameMismatchTest, CaptivePortalCertificateList_Enabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */, "" /* disabled */);
+
+ base::HistogramTester histograms;
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_EQ(1u, ssl_info().public_key_hashes.size());
+
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ ssl_info().public_key_hashes[0].ToString());
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ error_handler()->StartHandlingError();
+
+ // Timer shouldn't start for a known captive portal certificate.
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_FALSE(delegate()->captive_portal_checked());
+ EXPECT_FALSE(delegate()->ssl_interstitial_shown());
+ EXPECT_TRUE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ // A buggy SSL error handler might have incorrectly started the timer. Run to
+ // completion to ensure the timer is expired.
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_FALSE(delegate()->captive_portal_checked());
+ EXPECT_FALSE(delegate()->ssl_interstitial_shown());
+ EXPECT_TRUE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 3);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_CAPTIVE_PORTAL_INTERSTITIAL_OVERRIDABLE, 1);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::CAPTIVE_PORTAL_CERT_FOUND, 1);
+}
+
+// Tests that a certificate marked as a known captive portal certificate does
+// not cause the captive portal interstitial to be shown, if the feature is
+// disabled.
+TEST_F(SSLErrorHandlerNameMismatchTest, CaptivePortalCertificateList_Disabled) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "" /* enabled */, "CaptivePortalCertificateList" /* disabled */);
+
+ base::HistogramTester histograms;
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_EQ(1u, ssl_info().public_key_hashes.size());
+
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ ssl_info().public_key_hashes[0].ToString());
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ error_handler()->StartHandlingError();
+
+ // Timer shouldn't start for a known captive portal certificate.
+ EXPECT_TRUE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_TRUE(delegate()->captive_portal_checked());
+ EXPECT_FALSE(delegate()->ssl_interstitial_shown());
+ EXPECT_FALSE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ // A buggy SSL error handler might have incorrectly started the timer. Run to
+ // completion to ensure the timer is expired.
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_TRUE(delegate()->captive_portal_checked());
+ EXPECT_TRUE(delegate()->ssl_interstitial_shown());
+ EXPECT_FALSE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 2);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_SSL_INTERSTITIAL_OVERRIDABLE, 1);
+}
+
+// Tests that an error other than name mismatch does not cause a captive portal
+// interstitial to be shown, even if the certificate is marked as a known
+// captive portal certificate.
+TEST_F(SSLErrorHandlerAuthorityInvalidTest,
+ CaptivePortalCertificateList_ShouldShowGenericInterstitial) {
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitFromCommandLine(
+ "CaptivePortalCertificateList" /* enabled */, "" /* disabled */);
+
+ base::HistogramTester histograms;
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_EQ(1u, ssl_info().public_key_hashes.size());
+
+ chrome_browser_ssl::SSLErrorAssistantConfig config_proto;
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ ssl_info().public_key_hashes[0].ToString());
+ config_proto.add_captive_portal_cert()->set_sha256_hash(
+ "sha256/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+ SSLErrorHandler::SetErrorAssistantProtoForTesting(config_proto);
+
+ error_handler()->StartHandlingError();
+
+ // Timer should start for captive portal detection.
+ EXPECT_TRUE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_TRUE(delegate()->captive_portal_checked());
+ EXPECT_FALSE(delegate()->ssl_interstitial_shown());
+ EXPECT_FALSE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ base::RunLoop().RunUntilIdle();
+
+ EXPECT_FALSE(error_handler()->IsTimerRunningForTesting());
+ EXPECT_TRUE(delegate()->captive_portal_checked());
+ EXPECT_TRUE(delegate()->ssl_interstitial_shown());
+ EXPECT_FALSE(delegate()->captive_portal_interstitial_shown());
+ EXPECT_FALSE(delegate()->suggested_url_checked());
+
+ // Check that the histogram for the captive portal cert was recorded.
+ histograms.ExpectTotalCount(SSLErrorHandler::GetHistogramNameForTesting(), 2);
+ histograms.ExpectBucketCount(SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::HANDLE_ALL, 1);
+ histograms.ExpectBucketCount(
+ SSLErrorHandler::GetHistogramNameForTesting(),
+ SSLErrorHandler::SHOW_SSL_INTERSTITIAL_OVERRIDABLE, 1);
+}
+
+#endif // BUILDFLAG(ENABLE_CAPTIVE_PORTAL_DETECTION)