Commit ee03de14 authored by estark's avatar estark Committed by Commit bot

Process Expect-CT HTTP header

This CL processes the Expect-CT header when received on HTTP
responses. TransportSecurityState parses the header and, if valid and received
on a compliant connection, stores the Expect-CT state for the current host. (If
valid but received on a non-compliant connection, the header information is not
stored but a report is sent to alert the site owner of the misconfiguration.) A
follow-up CL will check the dynamic Expect-CT state on connection setup.

BUG=679012

Review-Url: https://codereview.chromium.org/2774763005
Cr-Commit-Position: refs/heads/master@{#467555}
parent d846315f
......@@ -1394,59 +1394,64 @@ void TransportSecurityState::ProcessExpectCTHeader(
const SSLInfo& ssl_info) {
DCHECK(CalledOnValidThread());
// Records the result of processing an Expect-CT header. This enum is
// histogrammed, so do not reorder or remove values.
enum ExpectCTHeaderResult {
// An Expect-CT header was received, but it had the wrong value.
EXPECT_CT_HEADER_BAD_VALUE = 0,
// The Expect-CT header was ignored because the build was old.
EXPECT_CT_HEADER_BUILD_NOT_TIMELY = 1,
// The Expect-CT header was ignored because the certificate did not chain to
// a public root.
EXPECT_CT_HEADER_PRIVATE_ROOT = 2,
// The Expect-CT header was ignored because CT compliance details were
// unavailable.
EXPECT_CT_HEADER_COMPLIANCE_DETAILS_UNAVAILABLE = 3,
// The request satisified the Expect-CT compliance policy, so no action was
// taken.
EXPECT_CT_HEADER_COMPLIED = 4,
// The Expect-CT header was ignored because there was no corresponding
// preload list entry.
EXPECT_CT_HEADER_NOT_PRELOADED = 5,
// The Expect-CT header was processed successfully and passed on to the
// delegate to send a report.
EXPECT_CT_HEADER_PROCESSED = 6,
EXPECT_CT_HEADER_LAST = EXPECT_CT_HEADER_PROCESSED
};
ExpectCTHeaderResult result = EXPECT_CT_HEADER_PROCESSED;
if (!expect_ct_reporter_)
// If a site sends `Expect-CT: preload` and appears on the preload list, they
// are in the experimental preload-list-only, report-only version of
// Expect-CT.
if (value == "preload") {
if (!expect_ct_reporter_)
return;
if (!IsBuildTimely())
return;
if (!ssl_info.is_issued_by_known_root)
return;
if (!ssl_info.ct_compliance_details_available)
return;
if (ssl_info.ct_cert_policy_compliance ==
ct::CertPolicyCompliance::CERT_POLICY_COMPLIES_VIA_SCTS)
return;
ExpectCTState state;
if (GetStaticExpectCTState(host_port_pair.host(), &state)) {
expect_ct_reporter_->OnExpectCTFailed(host_port_pair, state.report_uri,
ssl_info);
}
return;
ExpectCTState state;
if (value != "preload") {
result = EXPECT_CT_HEADER_BAD_VALUE;
} else if (!IsBuildTimely()) {
result = EXPECT_CT_HEADER_BUILD_NOT_TIMELY;
} else if (!ssl_info.is_issued_by_known_root) {
result = EXPECT_CT_HEADER_PRIVATE_ROOT;
} else if (!ssl_info.ct_compliance_details_available) {
result = EXPECT_CT_HEADER_COMPLIANCE_DETAILS_UNAVAILABLE;
} else if (ssl_info.ct_cert_policy_compliance ==
ct::CertPolicyCompliance::CERT_POLICY_COMPLIES_VIA_SCTS) {
result = EXPECT_CT_HEADER_COMPLIED;
} else if (!GetStaticExpectCTState(host_port_pair.host(), &state)) {
result = EXPECT_CT_HEADER_NOT_PRELOADED;
}
UMA_HISTOGRAM_ENUMERATION("Net.ExpectCTHeaderResult", result,
EXPECT_CT_HEADER_LAST + 1);
if (result != EXPECT_CT_HEADER_PROCESSED)
// Otherwise, see if the site has sent a valid Expect-CT header to dynamically
// turn on reporting and/or enforcement.
if (!IsDynamicExpectCTEnabled())
return;
expect_ct_reporter_->OnExpectCTFailed(host_port_pair, state.report_uri,
ssl_info);
base::Time now = base::Time::Now();
base::TimeDelta max_age;
bool enforce;
GURL report_uri;
if (!ParseExpectCTHeader(value, &max_age, &enforce, &report_uri))
return;
// Do not persist Expect-CT headers if the connection was not chained to a
// public root or did not comply with CT policy.
if (!ssl_info.is_issued_by_known_root)
return;
if (!ssl_info.ct_compliance_details_available)
return;
if (ssl_info.ct_cert_policy_compliance !=
ct::CertPolicyCompliance::CERT_POLICY_COMPLIES_VIA_SCTS) {
ExpectCTState state;
// If an Expect-CT header is observed over a non-compliant connection, the
// site owner should be notified about the misconfiguration. If the site was
// already opted in to Expect-CT, this report would have been sent at
// connection setup time. If the host is not already a noted Expect-CT host,
// however, the lack of CT compliance would not have been evaluated/reported
// at connection setup time, so it needs to be reported here while
// processing the header.
if (expect_ct_reporter_ && !report_uri.is_empty() &&
!GetDynamicExpectCTState(host_port_pair.host(), &state)) {
expect_ct_reporter_->OnExpectCTFailed(host_port_pair, report_uri,
ssl_info);
}
return;
}
AddExpectCTInternal(host_port_pair.host(), now, now + max_age, enforce,
report_uri);
}
// static
......
......@@ -508,14 +508,16 @@ class NET_EXPORT TransportSecurityState
const HostPortPair& host_port_pair,
const SSLInfo& ssl_info);
// Parses |value| as a Expect CT header value and sends an Expect CT
// report for |host_port_pair| if the following conditions are true:
// 1. The header value is "preload", indicating that the site wants to
// be opted in to Expect CT.
// 2. The given host is present on the Expect CT preload list with a
// valid report-uri, and the build is timely (i.e. preload list is fresh).
// 3. |ssl_info| indicates that the connection violated the Expect CT policy.
// 4. An Expect CT reporter has been provided with SetExpectCTReporter().
// Parses |value| as a Expect CT header value. If valid and served on a
// CT-compliant connection, adds an entry to the dynamic state. If valid but
// not served on a CT-compliant connection, a report is sent to alert the site
// owner of the misconfiguration (provided that a reporter has been set via
// SetExpectCTReporter).
//
// The header can also have the value "preload", indicating that the site
// wants to opt-in to the static report-only version of Expect-CT. If the
// given host is present on the preload list and the build is timely and the
// connection is not CT-compliant, then a report will be sent.
void ProcessExpectCTHeader(const std::string& value,
const HostPortPair& host_port_pair,
const SSLInfo& ssl_info);
......
......@@ -2692,4 +2692,116 @@ TEST_F(TransportSecurityStateTest, DynamicExpectCTStateDisabled) {
EXPECT_FALSE(state.GetDynamicExpectCTState(host, &expect_ct_state));
}
// Tests that dynamic Expect-CT opt-ins are processed correctly (when the
// feature is enabled).
TEST_F(TransportSecurityStateTest, DynamicExpectCT) {
const char kHeader[] = "max-age=123,enforce,report-uri=\"http://foo.test\"";
SSLInfo ssl;
ssl.is_issued_by_known_root = true;
ssl.ct_compliance_details_available = true;
ssl.ct_cert_policy_compliance =
ct::CertPolicyCompliance::CERT_POLICY_COMPLIES_VIA_SCTS;
// First test that the header is not processed when the feature is disabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(
TransportSecurityState::kDynamicExpectCTFeature);
TransportSecurityState state;
state.ProcessExpectCTHeader(kHeader, HostPortPair("example.test", 443),
ssl);
TransportSecurityState::ExpectCTState expect_ct_state;
EXPECT_FALSE(
state.GetDynamicExpectCTState("example.test", &expect_ct_state));
}
// Now test that the header is processed when the feature is enabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
TransportSecurityState::kDynamicExpectCTFeature);
base::Time now = base::Time::Now();
TransportSecurityState state;
MockExpectCTReporter reporter;
state.SetExpectCTReporter(&reporter);
state.ProcessExpectCTHeader(kHeader, HostPortPair("example.test", 443),
ssl);
TransportSecurityState::ExpectCTState expect_ct_state;
EXPECT_TRUE(
state.GetDynamicExpectCTState("example.test", &expect_ct_state));
EXPECT_EQ(GURL("http://foo.test"), expect_ct_state.report_uri);
EXPECT_TRUE(expect_ct_state.enforce);
EXPECT_LT(now, expect_ct_state.expiry);
// No report should be sent when the header was processed over a connection
// that complied with CT policy.
EXPECT_EQ(0u, reporter.num_failures());
}
}
// Tests that dynamic Expect-CT is not processed for private roots.
TEST_F(TransportSecurityStateTest, DynamicExpectCTPrivateRoot) {
const char kHeader[] = "max-age=123,enforce,report-uri=\"http://foo.test\"";
SSLInfo ssl;
ssl.is_issued_by_known_root = false;
ssl.ct_compliance_details_available = true;
ssl.ct_cert_policy_compliance =
ct::CertPolicyCompliance::CERT_POLICY_NOT_ENOUGH_SCTS;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
TransportSecurityState::kDynamicExpectCTFeature);
TransportSecurityState state;
MockExpectCTReporter reporter;
state.SetExpectCTReporter(&reporter);
state.ProcessExpectCTHeader(kHeader, HostPortPair("example.test", 443), ssl);
TransportSecurityState::ExpectCTState expect_ct_state;
EXPECT_FALSE(state.GetDynamicExpectCTState("example.test", &expect_ct_state));
EXPECT_EQ(0u, reporter.num_failures());
}
// Tests that dynamic Expect-CT is not processed when CT compliance status
// wasn't computed.
TEST_F(TransportSecurityStateTest, DynamicExpectCTNoComplianceDetails) {
const char kHeader[] = "max-age=123,enforce,report-uri=\"http://foo.test\"";
SSLInfo ssl;
ssl.is_issued_by_known_root = true;
ssl.ct_compliance_details_available = false;
ssl.ct_cert_policy_compliance =
ct::CertPolicyCompliance::CERT_POLICY_NOT_ENOUGH_SCTS;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
TransportSecurityState::kDynamicExpectCTFeature);
TransportSecurityState state;
MockExpectCTReporter reporter;
state.SetExpectCTReporter(&reporter);
state.ProcessExpectCTHeader(kHeader, HostPortPair("example.test", 443), ssl);
TransportSecurityState::ExpectCTState expect_ct_state;
EXPECT_FALSE(state.GetDynamicExpectCTState("example.test", &expect_ct_state));
EXPECT_EQ(0u, reporter.num_failures());
}
// Tests that Expect-CT reports are sent when an Expect-CT header is received
// over a non-compliant connection.
TEST_F(TransportSecurityStateTest, DynamicExpectCTNonCompliant) {
const char kHeader[] = "max-age=123,enforce,report-uri=\"http://foo.test\"";
SSLInfo ssl;
ssl.is_issued_by_known_root = true;
ssl.ct_compliance_details_available = true;
ssl.ct_cert_policy_compliance =
ct::CertPolicyCompliance::CERT_POLICY_NOT_ENOUGH_SCTS;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
TransportSecurityState::kDynamicExpectCTFeature);
TransportSecurityState state;
MockExpectCTReporter reporter;
state.SetExpectCTReporter(&reporter);
state.ProcessExpectCTHeader(kHeader, HostPortPair("example.test", 443), ssl);
TransportSecurityState::ExpectCTState expect_ct_state;
EXPECT_FALSE(state.GetDynamicExpectCTState("example.test", &expect_ct_state));
EXPECT_EQ(1u, reporter.num_failures());
EXPECT_EQ("example.test", reporter.host_port_pair().host());
}
} // namespace net
......@@ -34391,6 +34391,9 @@ http://cs/file:chrome/histograms.xml - but prefer this file for new entries.
</histogram>
<histogram name="Net.ExpectCTHeaderResult" enum="ExpectCTHeaderResult">
<obsolete>
Deprecated 04/2017.
</obsolete>
<owner>estark@chromium.org</owner>
<summary>
Sites can send an Expect-CT header to Chrome to indicate that they want a
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment