summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--android_webview/browser/aw_ssl_host_state_delegate.cc11
-rw-r--r--android_webview/browser/aw_ssl_host_state_delegate.h14
-rw-r--r--chrome/browser/ssl/chrome_ssl_host_state_delegate.h4
-rw-r--r--chrome/browser/ssl/ssl_browser_tests.cc160
-rw-r--r--content/browser/ssl/ssl_policy.cc25
-rw-r--r--content/browser/ssl/ssl_policy_backend.cc14
-rw-r--r--content/browser/ssl/ssl_policy_backend.h7
-rw-r--r--content/public/browser/ssl_host_state_delegate.h10
-rw-r--r--net/test/spawned_test_server/base_test_server.cc36
-rw-r--r--net/test/spawned_test_server/base_test_server.h8
-rw-r--r--tools/metrics/histograms/histograms.xml13
11 files changed, 262 insertions, 40 deletions
diff --git a/android_webview/browser/aw_ssl_host_state_delegate.cc b/android_webview/browser/aw_ssl_host_state_delegate.cc
index 59d717f..1da4747 100644
--- a/android_webview/browser/aw_ssl_host_state_delegate.cc
+++ b/android_webview/browser/aw_ssl_host_state_delegate.cc
@@ -86,4 +86,15 @@ SSLHostStateDelegate::CertJudgment AwSSLHostStateDelegate::QueryPolicy(
: SSLHostStateDelegate::DENIED;
}
+void AwSSLHostStateDelegate::RevokeUserAllowExceptions(
+ const std::string& host) {
+ cert_policy_for_host_.erase(host);
+}
+
+bool AwSSLHostStateDelegate::HasAllowException(const std::string& host) const {
+ auto policy_iterator = cert_policy_for_host_.find(host);
+ return policy_iterator != cert_policy_for_host_.end() &&
+ policy_iterator->second.HasAllowException();
+}
+
} // namespace android_webview
diff --git a/android_webview/browser/aw_ssl_host_state_delegate.h b/android_webview/browser/aw_ssl_host_state_delegate.h
index bc3e1a8..b5a34f4 100644
--- a/android_webview/browser/aw_ssl_host_state_delegate.h
+++ b/android_webview/browser/aw_ssl_host_state_delegate.h
@@ -30,6 +30,10 @@ class CertPolicy {
// remember the user's choice.
void Allow(const net::X509Certificate& cert, net::CertStatus error);
+ // Returns true if and only if there exists a user allow exception for some
+ // certificate.
+ bool HasAllowException() const { return allowed_.size() > 0; }
+
private:
// The set of fingerprints of allowed certificates.
std::map<net::SHA256HashValue, net::CertStatus, net::SHA256HashValueLessThan>
@@ -65,6 +69,16 @@ class AwSSLHostStateDelegate : public content::SSLHostStateDelegate {
bool DidHostRunInsecureContent(const std::string& host,
int pid) const override;
+ // Revokes all SSL certificate error allow exceptions made by the user for
+ // |host|.
+ void RevokeUserAllowExceptions(const std::string& host) override;
+
+ // Returns whether the user has allowed a certificate error exception for
+ // |host|. This does not mean that *all* certificate errors are allowed, just
+ // that there exists an exception. To see if a particular certificate and
+ // error combination exception is allowed, use QueryPolicy().
+ bool HasAllowException(const std::string& host) const override;
+
private:
// Certificate policies for each host.
std::map<std::string, internal::CertPolicy> cert_policy_for_host_;
diff --git a/chrome/browser/ssl/chrome_ssl_host_state_delegate.h b/chrome/browser/ssl/chrome_ssl_host_state_delegate.h
index e4e6480..b680740 100644
--- a/chrome/browser/ssl/chrome_ssl_host_state_delegate.h
+++ b/chrome/browser/ssl/chrome_ssl_host_state_delegate.h
@@ -41,7 +41,7 @@ class ChromeSSLHostStateDelegate : public content::SSLHostStateDelegate {
// Revokes all SSL certificate error allow exceptions made by the user for
// |host| in the given Profile.
- virtual void RevokeUserAllowExceptions(const std::string& host);
+ void RevokeUserAllowExceptions(const std::string& host) override;
// RevokeUserAllowExceptionsHard is the same as RevokeUserAllowExceptions but
// additionally may close idle connections in the process. This should be used
@@ -53,7 +53,7 @@ class ChromeSSLHostStateDelegate : public content::SSLHostStateDelegate {
// |host|. This does not mean that *all* certificate errors are allowed, just
// that there exists an exception. To see if a particular certificate and
// error combination exception is allowed, use QueryPolicy().
- virtual bool HasAllowException(const std::string& host) const;
+ bool HasAllowException(const std::string& host) const override;
protected:
// SetClock takes ownership of the passed in clock.
diff --git a/chrome/browser/ssl/ssl_browser_tests.cc b/chrome/browser/ssl/ssl_browser_tests.cc
index e7ded66..59da844 100644
--- a/chrome/browser/ssl/ssl_browser_tests.cc
+++ b/chrome/browser/ssl/ssl_browser_tests.cc
@@ -21,6 +21,7 @@
#include "chrome/browser/safe_browsing/ping_manager.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/safe_browsing/ui_manager.h"
+#include "chrome/browser/ssl/chrome_ssl_host_state_delegate.h"
#include "chrome/browser/ssl/ssl_blocking_page.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
@@ -41,6 +42,7 @@
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/render_frame_host.h"
+#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
@@ -50,13 +52,19 @@
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/download_test_observer.h"
#include "content/public/test/test_renderer_host.h"
+#include "net/base/host_port_pair.h"
#include "net/base/net_errors.h"
#include "net/base/test_data_directory.h"
#include "net/cert/cert_status_flags.h"
+#include "net/cert/test_root_certs.h"
#include "net/cert/x509_certificate.h"
+#include "net/dns/host_resolver.h"
+#include "net/dns/mock_host_resolver.h"
+#include "net/http/http_transaction_factory.h"
#include "net/ssl/ssl_info.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "net/url_request/url_request_context.h"
+#include "net/url_request/url_request_context_getter.h"
#if defined(USE_NSS_CERTS)
#include "chrome/browser/net/nss_context.h"
@@ -265,6 +273,33 @@ class MockSSLCertReporter : public SSLCertReporter {
} // namespace CertificateReporting
+void RootCertsChangedOnIOThread(
+ const scoped_refptr<net::URLRequestContextGetter> context_getter) {
+ net::CertDatabase::GetInstance()->NotifyObserversOfCACertChanged(NULL);
+ context_getter->GetURLRequestContext()
+ ->http_transaction_factory()
+ ->GetSession()
+ ->CloseAllConnections();
+}
+
+// Alerts the URLRequestContext for the given WebContents that a root
+// certificate has changed state or been removed. This, in turn, clears any
+// cached certificate validation in the cert verifier. This will also close all
+// connections in the socket pool of |contents|, so calls to this should be made
+// with care.
+void RootCertsChanged(WebContents* contents) {
+ scoped_refptr<net::URLRequestContextGetter> url_request_context =
+ contents->GetBrowserContext()->GetRequestContextForRenderProcess(
+ contents->GetRenderProcessHost()->GetID());
+ base::RunLoop run_loop;
+ content::BrowserThread::PostTaskAndReply(
+ content::BrowserThread::IO, FROM_HERE,
+ base::Bind(&RootCertsChangedOnIOThread, url_request_context),
+ run_loop.QuitClosure());
+ run_loop.Run();
+ base::RunLoop().RunUntilIdle();
+}
+
} // namespace
class SSLUITest : public InProcessBrowserTest {
@@ -438,11 +473,11 @@ class SSLUITest : public InProcessBrowserTest {
}
static bool GetPageWithUnsafeWorkerPath(
- const net::SpawnedTestServer& expired_https_server,
+ const net::SpawnedTestServer& https_server,
std::string* page_with_unsafe_worker_path) {
// Get the "imported.js" URL from the expired https server and
// substitute it into the unsafe_worker.js file.
- GURL imported_js_url = expired_https_server.GetURL("files/ssl/imported.js");
+ GURL imported_js_url = https_server.GetURL("files/ssl/imported.js");
std::vector<net::SpawnedTestServer::StringPair>
replacement_text_for_unsafe_worker;
replacement_text_for_unsafe_worker.push_back(
@@ -2032,32 +2067,93 @@ IN_PROC_BROWSER_TEST_F(SSLUITest, TestUnsafeContentsInWorkerFiltered) {
CheckAuthenticatedState(tab, AuthState::NONE);
}
-IN_PROC_BROWSER_TEST_F(SSLUITest, TestUnsafeContentsInWorker) {
+// This test, and the related test TestUnsafeContentsWithUserException, verify
+// that if unsafe content is loaded but the host of that unsafe content has a
+// user exception, the content runs and the security style remains
+// authenticated. This is not necessarily the behavior that should exist, but it
+// is verification that it does behave that way. See https://crbug.com/477868
+// for more inforamtion on this.
+IN_PROC_BROWSER_TEST_F(SSLUITest, TestUnsafeContentsInWorkerWithUserException) {
ASSERT_TRUE(https_server_.Start());
- ASSERT_TRUE(https_server_expired_.Start());
+ // Note that it is necessary to user https_server_mismatched_ here over the
+ // other invalid cert servers. This is because the test relies on the two
+ // servers having different hosts since SSL exceptions are per-host, not per
+ // origin, and https_server_mismatched_ uses 'localhost' rather than
+ // '127.0.0.1'.
+ ASSERT_TRUE(https_server_mismatched_.Start());
// Navigate to an unsafe site. Proceed with interstitial page to indicate
// the user approves the bad certificate.
- ui_test_utils::NavigateToURL(browser(),
- https_server_expired_.GetURL("files/ssl/blank_page.html"));
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_mismatched_.GetURL("files/ssl/blank_page.html"));
WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
- CheckAuthenticationBrokenState(
- tab, net::CERT_STATUS_DATE_INVALID, AuthState::SHOWING_INTERSTITIAL);
+ CheckAuthenticationBrokenState(tab, net::CERT_STATUS_COMMON_NAME_INVALID,
+ AuthState::SHOWING_INTERSTITIAL);
ProceedThroughInterstitial(tab);
- CheckAuthenticationBrokenState(
- tab, net::CERT_STATUS_DATE_INVALID, AuthState::NONE);
+ CheckAuthenticationBrokenState(tab, net::CERT_STATUS_COMMON_NAME_INVALID,
+ AuthState::NONE);
// Navigate to safe page that has Worker loading unsafe content.
// Expect content to load but be marked as auth broken due to running insecure
// content.
std::string page_with_unsafe_worker_path;
- ASSERT_TRUE(GetPageWithUnsafeWorkerPath(https_server_expired_,
+ ASSERT_TRUE(GetPageWithUnsafeWorkerPath(https_server_mismatched_,
&page_with_unsafe_worker_path));
- ui_test_utils::NavigateToURL(browser(), https_server_.GetURL(
- page_with_unsafe_worker_path));
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_.GetURL(page_with_unsafe_worker_path));
CheckWorkerLoadResult(tab, true); // Worker loads insecure content
- CheckAuthenticationBrokenState(
- tab, CertError::NONE, AuthState::RAN_INSECURE_CONTENT);
+ CheckAuthenticatedState(tab, CertError::NONE);
+}
+
+// Visits a page with unsafe content and makes sure that if a user exception to
+// the certificate error is present, the image is loaded and script executes.
+//
+// See the comment above SSLUITest.TestUnsafeContentsInWorkerWithUserException
+// for a discussion about the desired behavior.
+IN_PROC_BROWSER_TEST_F(SSLUITest, TestUnsafeContentsWithUserException) {
+ ASSERT_TRUE(https_server_.Start());
+ // Note that it is necessary to user https_server_mismatched_ here over the
+ // other invalid cert servers. This is because the test relies on the two
+ // servers having different hosts since SSL exceptions are per-host, not per
+ // origin, and https_server_mismatched_ uses 'localhost' rather than
+ // '127.0.0.1'.
+ ASSERT_TRUE(https_server_mismatched_.Start());
+
+ // Navigate to an unsafe site. Proceed with interstitial page to indicate
+ // the user approves the bad certificate.
+ ui_test_utils::NavigateToURL(
+ browser(), https_server_mismatched_.GetURL("files/ssl/blank_page.html"));
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ CheckAuthenticationBrokenState(tab, net::CERT_STATUS_COMMON_NAME_INVALID,
+ AuthState::SHOWING_INTERSTITIAL);
+ ProceedThroughInterstitial(tab);
+ CheckAuthenticationBrokenState(tab, net::CERT_STATUS_COMMON_NAME_INVALID,
+ AuthState::NONE);
+
+ std::string replacement_path;
+ ASSERT_TRUE(GetFilePathWithHostAndPortReplacement(
+ "files/ssl/page_with_unsafe_contents.html",
+ https_server_mismatched_.host_port_pair(), &replacement_path));
+ ui_test_utils::NavigateToURL(browser(),
+ https_server_.GetURL(replacement_path));
+
+ // When the bad content is filtered, the state is expected to be
+ // authenticated.
+ CheckAuthenticatedState(tab, AuthState::NONE);
+
+ int img_width;
+ EXPECT_TRUE(content::ExecuteScriptAndExtractInt(
+ tab, "window.domAutomationController.send(ImageWidth());", &img_width));
+ // In order to check that the image was loaded, we check its width.
+ // The actual image (Google logo) is 114 pixels wide, so we assume a good
+ // image is greater than 100.
+ EXPECT_GT(img_width, 100);
+
+ bool js_result = false;
+ EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
+ tab, "window.domAutomationController.send(IsFooSet());", &js_result));
+ EXPECT_TRUE(js_result);
+ CheckAuthenticatedState(tab, CertError::NONE);
}
// Test that when the browser blocks displaying insecure content (images), the
@@ -2248,6 +2344,40 @@ IN_PROC_BROWSER_TEST_F(SSLUITest, InterstitialNotAffectedByHideShow) {
EXPECT_TRUE(tab->GetRenderWidgetHostView()->IsShowing());
}
+// Verifies that if a bad certificate is seen for a host and the user proceeds
+// through the interstitial, the decision to proceed is initially remembered.
+// However, if this is followed by another visit, and a good certificate
+// is seen for the same host, the original exception is forgotten.
+IN_PROC_BROWSER_TEST_F(SSLUITest, BadCertFollowedByGoodCert) {
+ ASSERT_TRUE(https_server_.Start());
+ std::string https_server_host =
+ https_server_.GetURL("files/ssl/google.html").host();
+
+ WebContents* tab = browser()->tab_strip_model()->GetActiveWebContents();
+ net::TestRootCerts* root_certs = net::TestRootCerts::GetInstance();
+
+ ASSERT_TRUE(root_certs);
+ root_certs->Clear();
+
+ Profile* profile = Profile::FromBrowserContext(tab->GetBrowserContext());
+ ChromeSSLHostStateDelegate* state =
+ reinterpret_cast<ChromeSSLHostStateDelegate*>(
+ profile->GetSSLHostStateDelegate());
+
+ ui_test_utils::NavigateToURL(browser(),
+ https_server_.GetURL("files/ssl/google.html"));
+
+ ProceedThroughInterstitial(tab);
+ EXPECT_TRUE(state->HasAllowException(https_server_host));
+
+ ASSERT_TRUE(https_server_.LoadTestRootCert());
+ RootCertsChanged(tab);
+ ui_test_utils::NavigateToURL(browser(),
+ https_server_.GetURL("files/ssl/google.html"));
+ ASSERT_FALSE(tab->GetInterstitialPage());
+ EXPECT_FALSE(state->HasAllowException(https_server_host));
+}
+
class SSLBlockingPageIDNTest : public SecurityInterstitialIDNTest {
protected:
// SecurityInterstitialIDNTest implementation
diff --git a/content/browser/ssl/ssl_policy.cc b/content/browser/ssl/ssl_policy.cc
index 610f741..5a627fb 100644
--- a/content/browser/ssl/ssl_policy.cc
+++ b/content/browser/ssl/ssl_policy.cc
@@ -8,6 +8,7 @@
#include "base/bind.h"
#include "base/command_line.h"
#include "base/memory/singleton.h"
+#include "base/metrics/histogram_macros.h"
#include "base/strings/string_piece.h"
#include "base/strings/string_util.h"
#include "content/browser/frame_host/navigation_entry_impl.h"
@@ -26,6 +27,16 @@
namespace content {
+namespace {
+
+// Events for UMA. Do not reorder or change!
+enum SSLGoodCertSeenEvent {
+ NO_PREVIOUS_EXCEPTION = 0,
+ HAD_PREVIOUS_EXCEPTION = 1,
+ SSL_GOOD_CERT_SEEN_EVENT_MAX = 2
+};
+}
+
SSLPolicy::SSLPolicy(SSLPolicyBackend* backend)
: backend_(backend) {
DCHECK(backend_);
@@ -110,8 +121,20 @@ void SSLPolicy::OnRequestStarted(SSLRequestInfo* info) {
// this information back through WebKit and out some FrameLoaderClient
// methods.
- if (net::IsCertStatusError(info->ssl_cert_status()))
+ if (net::IsCertStatusError(info->ssl_cert_status())) {
backend_->HostRanInsecureContent(info->url().host(), info->child_id());
+ } else {
+ SSLGoodCertSeenEvent event = NO_PREVIOUS_EXCEPTION;
+ if (backend_->HasAllowException(info->url().host())) {
+ // If there's no certificate error, a good certificate has been seen, so
+ // clear out any exceptions that were made by the user for bad
+ // certificates.
+ backend_->RevokeUserAllowExceptions(info->url().host());
+ event = HAD_PREVIOUS_EXCEPTION;
+ }
+ UMA_HISTOGRAM_ENUMERATION("interstitial.ssl.good_cert_seen", event,
+ SSL_GOOD_CERT_SEEN_EVENT_MAX);
+ }
}
void SSLPolicy::UpdateEntry(NavigationEntryImpl* entry,
diff --git a/content/browser/ssl/ssl_policy_backend.cc b/content/browser/ssl/ssl_policy_backend.cc
index 5c65874..a2626da 100644
--- a/content/browser/ssl/ssl_policy_backend.cc
+++ b/content/browser/ssl/ssl_policy_backend.cc
@@ -31,6 +31,20 @@ bool SSLPolicyBackend::DidHostRunInsecureContent(const std::string& host,
return ssl_host_state_delegate_->DidHostRunInsecureContent(host, pid);
}
+void SSLPolicyBackend::RevokeUserAllowExceptions(const std::string& host) {
+ if (!ssl_host_state_delegate_)
+ return;
+
+ ssl_host_state_delegate_->RevokeUserAllowExceptions(host);
+}
+
+bool SSLPolicyBackend::HasAllowException(const std::string& host) {
+ if (!ssl_host_state_delegate_)
+ return false;
+
+ return ssl_host_state_delegate_->HasAllowException(host);
+}
+
void SSLPolicyBackend::AllowCertForHost(const net::X509Certificate& cert,
const std::string& host,
net::CertStatus error) {
diff --git a/content/browser/ssl/ssl_policy_backend.h b/content/browser/ssl/ssl_policy_backend.h
index 15ebe31..ed50c24 100644
--- a/content/browser/ssl/ssl_policy_backend.h
+++ b/content/browser/ssl/ssl_policy_backend.h
@@ -27,6 +27,13 @@ class SSLPolicyBackend {
// Returns whether the specified host ran insecure content.
bool DidHostRunInsecureContent(const std::string& host, int pid) const;
+ // Revokes all allow exceptions by the user for |host|.
+ void RevokeUserAllowExceptions(const std::string& host);
+
+ // Returns true if and only if a user exception has previously been made for
+ // |host|.
+ bool HasAllowException(const std::string& host);
+
// Records that |cert| is permitted to be used for |host| in the future, for
// a specific error type.
void AllowCertForHost(const net::X509Certificate& cert,
diff --git a/content/public/browser/ssl_host_state_delegate.h b/content/public/browser/ssl_host_state_delegate.h
index 4666471..39589a5 100644
--- a/content/public/browser/ssl_host_state_delegate.h
+++ b/content/public/browser/ssl_host_state_delegate.h
@@ -54,6 +54,16 @@ class SSLHostStateDelegate {
virtual bool DidHostRunInsecureContent(const std::string& host,
int pid) const = 0;
+ // Revokes all SSL certificate error allow exceptions made by the user for
+ // |host|.
+ virtual void RevokeUserAllowExceptions(const std::string& host) = 0;
+
+ // Returns whether the user has allowed a certificate error exception for
+ // |host|. This does not mean that *all* certificate errors are allowed, just
+ // that there exists an exception. To see if a particular certificate and
+ // error combination exception is allowed, use QueryPolicy().
+ virtual bool HasAllowException(const std::string& host) const = 0;
+
protected:
virtual ~SSLHostStateDelegate() {}
};
diff --git a/net/test/spawned_test_server/base_test_server.cc b/net/test/spawned_test_server/base_test_server.cc
index 3eeb685..b8ded1f 100644
--- a/net/test/spawned_test_server/base_test_server.cc
+++ b/net/test/spawned_test_server/base_test_server.cc
@@ -310,6 +310,24 @@ bool BaseTestServer::GetFilePathWithReplacements(
return true;
}
+bool BaseTestServer::LoadTestRootCert() const {
+ TestRootCerts* root_certs = TestRootCerts::GetInstance();
+ if (!root_certs)
+ return false;
+
+ // Should always use absolute path to load the root certificate.
+ base::FilePath root_certificate_path = certificates_dir_;
+ if (!certificates_dir_.IsAbsolute()) {
+ base::FilePath src_dir;
+ if (!PathService::Get(base::DIR_SOURCE_ROOT, &src_dir))
+ return false;
+ root_certificate_path = src_dir.Append(certificates_dir_);
+ }
+
+ return root_certs->AddFromFile(
+ root_certificate_path.AppendASCII("root_ca_cert.pem"));
+}
+
void BaseTestServer::Init(const std::string& host) {
host_port_pair_ = HostPortPair(host, 0);
@@ -352,24 +370,6 @@ bool BaseTestServer::ParseServerData(const std::string& server_data) {
return true;
}
-bool BaseTestServer::LoadTestRootCert() const {
- TestRootCerts* root_certs = TestRootCerts::GetInstance();
- if (!root_certs)
- return false;
-
- // Should always use absolute path to load the root certificate.
- base::FilePath root_certificate_path = certificates_dir_;
- if (!certificates_dir_.IsAbsolute()) {
- base::FilePath src_dir;
- if (!PathService::Get(base::DIR_SOURCE_ROOT, &src_dir))
- return false;
- root_certificate_path = src_dir.Append(certificates_dir_);
- }
-
- return root_certs->AddFromFile(
- root_certificate_path.AppendASCII("root_ca_cert.pem"));
-}
-
bool BaseTestServer::SetupWhenServerStarted() {
DCHECK(host_port_pair_.port());
diff --git a/net/test/spawned_test_server/base_test_server.h b/net/test/spawned_test_server/base_test_server.h
index 5fd57f1..d430b0e 100644
--- a/net/test/spawned_test_server/base_test_server.h
+++ b/net/test/spawned_test_server/base_test_server.h
@@ -262,6 +262,10 @@ class BaseTestServer {
ws_basic_auth_ = ws_basic_auth;
}
+ // Marks the root certificate of an HTTPS test server as trusted for
+ // the duration of tests.
+ bool LoadTestRootCert() const WARN_UNUSED_RESULT;
+
protected:
virtual ~BaseTestServer();
Type type() const { return type_; }
@@ -300,10 +304,6 @@ class BaseTestServer {
private:
void Init(const std::string& host);
- // Marks the root certificate of an HTTPS test server as trusted for
- // the duration of tests.
- bool LoadTestRootCert() const WARN_UNUSED_RESULT;
-
// Document root of the test server.
base::FilePath document_root_;
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index efb9be2..2e0e9c7 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -13096,6 +13096,14 @@ Therefore, the affected-histogram name has to have at least one dot in it.
</summary>
</histogram>
+<histogram name="interstitial.ssl.good_cert_seen" enum="SSLGoodCertSeenEvent">
+ <owner>jww@chromium.org</owner>
+ <summary>
+ Emitted when a good certificate is seen, specifying whether the user already
+ gave an exception for a bad certificate for the same host.
+ </summary>
+</histogram>
+
<histogram name="interstitial.ssl.severity_score.authority_invalid" units="%">
<obsolete>
Deprecated Jan 2015 (M42).
@@ -62454,6 +62462,11 @@ To add a new entry, add it with any value and run test to compute valid value.
<int value="9" label="UNKNOWN"/>
</enum>
+<enum name="SSLGoodCertSeenEvent" type="int">
+ <int value="0" label="NO_PREVIOUS_EXCEPTION"/>
+ <int value="1" label="HAD_PREVIOUS_EXCEPTION"/>
+</enum>
+
<enum name="SSLIsExpiredAndDecision" type="int">
<int value="0" label="EXPIRED_AND_PROCEED"/>
<int value="1" label="EXPIRED_AND_DO_NOT_PROCEED"/>