0

Add DNS name constraints support to Chrome Root Store constraints

Bug: 40941039
Change-Id: I9a66f35b718cf65c45ade666707c1e5e3898a861
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5943563
Reviewed-by: Joshua Pawlicki <waffles@chromium.org>
Reviewed-by: Hubert Chao <hchao@chromium.org>
Commit-Queue: Matt Mueller <mattm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1372832}
This commit is contained in:
Matt Mueller
2024-10-23 17:59:05 +00:00
committed by Chromium LUCI CQ
parent dd890d94b0
commit 2367275f29
9 changed files with 247 additions and 19 deletions

@ -599,13 +599,7 @@ class PKIMetadataComponentChromeRootStoreUpdateTest
raw_ptr<PKIMetadataComponentChromeRootStoreUpdateTest> test_;
};
void InstallCRSUpdate(const std::vector<std::string>& der_roots) {
chrome_root_store::RootStore root_store_proto;
root_store_proto.set_version_major(++last_used_crs_version_);
for (const auto& der_root : der_roots) {
root_store_proto.add_trust_anchors()->set_der(der_root);
}
void InstallCRSUpdate(chrome_root_store::RootStore root_store_proto) {
{
base::ScopedAllowBlockingForTesting allow_blocking;
ASSERT_TRUE(
@ -620,6 +614,16 @@ class PKIMetadataComponentChromeRootStoreUpdateTest
waiter.Wait();
}
void InstallCRSUpdate(const std::vector<std::string>& der_roots) {
chrome_root_store::RootStore root_store_proto;
root_store_proto.set_version_major(++last_used_crs_version_);
for (const auto& der_root : der_roots) {
root_store_proto.add_trust_anchors()->set_der(der_root);
}
InstallCRSUpdate(std::move(root_store_proto));
}
protected:
base::ScopedTempDir component_dir_;
@ -763,6 +767,83 @@ IN_PROC_BROWSER_TEST_F(PKIMetadataComponentChromeRootStoreUpdateTest,
ssl_test_util::AuthState::SHOWING_INTERSTITIAL);
}
IN_PROC_BROWSER_TEST_F(PKIMetadataComponentChromeRootStoreUpdateTest,
CheckCRSUpdateDnsConstraint) {
net::EmbeddedTestServer https_server_ok(net::EmbeddedTestServer::TYPE_HTTPS);
net::EmbeddedTestServer::ServerCertificateConfig server_config;
server_config.dns_names = {"*.example.com"};
https_server_ok.SetSSLConfig(server_config);
https_server_ok.ServeFilesFromSourceDirectory("chrome/test/data");
// Clear test roots so that cert validation only happens with
// what's in Chrome Root Store.
net::TestRootCerts::GetInstance()->Clear();
ASSERT_TRUE(https_server_ok.Start());
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_ok.GetURL("a.example.com", "/simple.html")));
// The page should be blocked as the test root is not trusted yet.
content::WebContents* tab = chrome_test_utils::GetActiveWebContents(this);
ASSERT_TRUE(WaitForRenderFrameReady(tab->GetPrimaryMainFrame()));
EXPECT_NE(chrome_test_utils::GetActiveWebContents(this)->GetTitle(), u"OK");
ssl_test_util::CheckAuthenticationBrokenState(
tab, net::CERT_STATUS_AUTHORITY_INVALID,
ssl_test_util::AuthState::SHOWING_INTERSTITIAL);
int64_t crs_version = net::CompiledChromeRootStoreVersion();
scoped_refptr<net::X509Certificate> root_cert =
net::ImportCertFromFile(net::EmbeddedTestServer::GetRootCertPemPath());
ASSERT_TRUE(root_cert);
// Install CRS update that trusts root with a constraint that matches the
// leaf's subjectAltName.
{
chrome_root_store::RootStore root_store_proto;
root_store_proto.set_version_major(++crs_version);
chrome_root_store::TrustAnchor* anchor =
root_store_proto.add_trust_anchors();
anchor->set_der(std::string(
net::x509_util::CryptoBufferAsStringPiece(root_cert->cert_buffer())));
anchor->add_constraints()->add_permitted_dns_names("example.com");
InstallCRSUpdate(std::move(root_store_proto));
}
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_ok.GetURL("b.example.com", "/simple.html")));
// Check that the page is allowed now.
tab = chrome_test_utils::GetActiveWebContents(this);
ASSERT_TRUE(WaitForRenderFrameReady(tab->GetPrimaryMainFrame()));
EXPECT_EQ(chrome_test_utils::GetActiveWebContents(this)->GetTitle(), u"OK");
ssl_test_util::CheckAuthenticatedState(tab, ssl_test_util::AuthState::NONE);
// Install CRS update that trusts root with a constraint that does not match
// the leaf's subjectAltName.
{
chrome_root_store::RootStore root_store_proto;
root_store_proto.set_version_major(++crs_version);
chrome_root_store::TrustAnchor* anchor =
root_store_proto.add_trust_anchors();
anchor->set_der(std::string(
net::x509_util::CryptoBufferAsStringPiece(root_cert->cert_buffer())));
anchor->add_constraints()->add_permitted_dns_names("example.org");
InstallCRSUpdate(std::move(root_store_proto));
}
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), https_server_ok.GetURL("c.example.com", "/simple.html")));
// Check that the page is blocked now.
tab = chrome_test_utils::GetActiveWebContents(this);
ASSERT_TRUE(WaitForRenderFrameReady(tab->GetPrimaryMainFrame()));
EXPECT_NE(chrome_test_utils::GetActiveWebContents(this)->GetTitle(), u"OK");
ssl_test_util::CheckAuthenticationBrokenState(
tab, net::CERT_STATUS_AUTHORITY_INVALID,
ssl_test_util::AuthState::SHOWING_INTERSTITIAL);
}
// Test suite for tests that depend on both Certificate Transparency and Chrome
// Root Store updates.
class PKIMetadataComponentCtAndCrsUpdaterTest

@ -477,6 +477,30 @@ class PathBuilderDelegateImpl : public bssl::SimplePathBuilderDelegate {
}
}
if (!constraint.permitted_dns_names.empty()) {
bssl::GeneralNames permitted_names;
for (const auto& dns_name : constraint.permitted_dns_names) {
permitted_names.dns_names.push_back(dns_name);
}
permitted_names.present_name_types |=
bssl::GeneralNameTypes::GENERAL_NAME_DNS_NAME;
std::unique_ptr<bssl::NameConstraints> nc =
bssl::NameConstraints::CreateFromPermittedSubtrees(
std::move(permitted_names));
const std::shared_ptr<const bssl::ParsedCertificate>& leaf_cert =
path->certs[0];
bssl::CertErrors name_constraint_errors;
nc->IsPermittedCert(leaf_cert->normalized_subject(),
leaf_cert->subject_alt_names(),
&name_constraint_errors);
if (name_constraint_errors.ContainsAnyErrorWithSeverity(
bssl::CertError::SEVERITY_HIGH)) {
return false;
}
}
if (constraint.min_version.has_value() &&
version_info::GetVersion() < constraint.min_version.value()) {
return false;

@ -1659,6 +1659,51 @@ TEST_F(CertVerifyProcBuiltinTest, ChromeRootStoreConstraintMinAndMaxVersion) {
}
}
TEST_F(CertVerifyProcBuiltinTest, ChromeRootStoreConstraintNameConstraints) {
auto [leaf, root] = CertBuilder::CreateSimpleChain2();
ScopedTestRoot scoped_root(root->GetX509Certificate());
// If the the CRS root has dns name constraints and the cert's names don't
// match the name constraints, verification should fail.
{
std::array<std::string_view, 2> permitted_dns_names = {
std::string_view("example.org"),
std::string_view("foo.example.com"),
};
SetMockChromeRootConstraints(
{{.permitted_dns_names = permitted_dns_names}});
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(leaf->GetX509Certificate(), "www.example.com",
/*flags=*/0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
EXPECT_THAT(error, IsError(ERR_CERT_AUTHORITY_INVALID));
}
// If cert's names match the CRS name constraints, verification should
// succeed.
{
std::array<std::string_view, 2> permitted_dns_names = {
std::string_view("example.org"),
std::string_view("example.com"),
};
SetMockChromeRootConstraints(
{{.permitted_dns_names = permitted_dns_names}});
CertVerifyResult verify_result;
NetLogSource verify_net_log_source;
TestCompletionCallback callback;
Verify(leaf->GetX509Certificate(), "www.example.com",
/*flags=*/0, &verify_result, &verify_net_log_source,
callback.callback());
int error = callback.WaitForResult();
EXPECT_THAT(error, IsOk());
}
}
// Tests multiple constraint objects in the constraints vector. The CRS
// constraints are satisfied if at least one of the constraint objects is
// satisfied.

@ -8,6 +8,7 @@
#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
@ -32,17 +33,23 @@ ChromeRootCertConstraints::ChromeRootCertConstraints(
std::optional<base::Time> sct_not_after,
std::optional<base::Time> sct_all_after,
std::optional<base::Version> min_version,
std::optional<base::Version> max_version_exclusive)
std::optional<base::Version> max_version_exclusive,
std::vector<std::string> permitted_dns_names)
: sct_not_after(sct_not_after),
sct_all_after(sct_all_after),
min_version(std::move(min_version)),
max_version_exclusive(std::move(max_version_exclusive)) {}
max_version_exclusive(std::move(max_version_exclusive)),
permitted_dns_names(std::move(permitted_dns_names)) {}
ChromeRootCertConstraints::ChromeRootCertConstraints(
const StaticChromeRootCertConstraints& constraints)
: sct_not_after(constraints.sct_not_after),
sct_all_after(constraints.sct_all_after),
min_version(constraints.min_version),
max_version_exclusive(constraints.max_version_exclusive) {
for (std::string_view name : constraints.permitted_dns_names) {
permitted_dns_names.emplace_back(name);
}
if (min_version) {
CHECK(min_version->IsValid());
}
@ -50,6 +57,7 @@ ChromeRootCertConstraints::ChromeRootCertConstraints(
CHECK(max_version_exclusive->IsValid());
}
}
ChromeRootCertConstraints::~ChromeRootCertConstraints() = default;
ChromeRootCertConstraints::ChromeRootCertConstraints(
const ChromeRootCertConstraints& other) = default;
@ -134,7 +142,8 @@ ChromeRootStoreData::CreateChromeRootStoreData(
? std::optional(base::Time::UnixEpoch() +
base::Seconds(constraint.sct_all_after_sec()))
: std::nullopt,
min_version, max_version_exclusive);
min_version, max_version_exclusive,
base::ToVector(constraint.permitted_dns_names()));
}
root_store_data.anchors_.emplace_back(std::move(parsed),
std::move(constraints));
@ -298,6 +307,8 @@ TrustStoreChrome::ParseCrsConstraintsSwitch(std::string_view switch_value) {
continue;
}
constraint.max_version_exclusive = version;
} else if (constraint_name_lower == "dns") {
constraint.permitted_dns_names.push_back(constraint_value);
} else {
LOG(ERROR) << "unrecognized constraint " << constraint_name_lower;
}

@ -31,6 +31,8 @@ struct StaticChromeRootCertConstraints {
std::optional<std::string_view> min_version;
std::optional<std::string_view> max_version_exclusive;
base::span<const std::string_view> permitted_dns_names;
};
struct ChromeRootCertInfo {
@ -43,7 +45,8 @@ struct NET_EXPORT ChromeRootCertConstraints {
ChromeRootCertConstraints(std::optional<base::Time> sct_not_after,
std::optional<base::Time> sct_all_after,
std::optional<base::Version> min_version,
std::optional<base::Version> max_version_exclusive);
std::optional<base::Version> max_version_exclusive,
std::vector<std::string> permitted_dns_names);
explicit ChromeRootCertConstraints(
const StaticChromeRootCertConstraints& constraints);
~ChromeRootCertConstraints();
@ -57,6 +60,8 @@ struct NET_EXPORT ChromeRootCertConstraints {
std::optional<base::Version> min_version;
std::optional<base::Version> max_version_exclusive;
std::vector<std::string> permitted_dns_names;
};
// ChromeRootStoreData is a container class that stores all of the Chrome Root
@ -119,6 +124,7 @@ class NET_EXPORT TrustStoreChrome : public bssl::TrustStore {
// `sctallafter=${seconds_since_epoch}`
// `minversion=${dotted_version_string}`
// `maxversionexclusive=${dotted_version_string}`
// `dns=${permitted_dns_name}` (can be specified multiple times)
//
// If the same root hash is specified multiple times in separate constraint
// specifications, each time will create a new constraintset for that root,

@ -13,6 +13,7 @@
#include "net/test/cert_builder.h"
#include "net/test/cert_test_util.h"
#include "net/test/test_data_directory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/boringssl/src/pki/cert_errors.h"
#include "third_party/boringssl/src/pki/parsed_certificate.h"
@ -117,7 +118,7 @@ TEST(TrustStoreChromeTestNoFixture, Constraints) {
ASSERT_TRUE(constrained_cert);
base::span<const ChromeRootCertConstraints> constraints =
trust_store_chrome->GetConstraintsForCert(constrained_cert.get());
ASSERT_EQ(constraints.size(), 2U);
ASSERT_EQ(constraints.size(), 3U);
EXPECT_FALSE(constraints[0].sct_all_after.has_value());
ASSERT_TRUE(constraints[0].sct_not_after.has_value());
@ -129,6 +130,8 @@ TEST(TrustStoreChromeTestNoFixture, Constraints) {
ASSERT_TRUE(constraints[0].max_version_exclusive.has_value());
EXPECT_EQ(constraints[0].max_version_exclusive.value().components(),
std::vector<uint32_t>({125, 0, 6368, 2}));
EXPECT_THAT(constraints[0].permitted_dns_names,
testing::ElementsAre("foo.example.com", "bar.example.com"));
EXPECT_FALSE(constraints[1].sct_not_after.has_value());
ASSERT_TRUE(constraints[1].sct_all_after.has_value());
@ -140,6 +143,10 @@ TEST(TrustStoreChromeTestNoFixture, Constraints) {
EXPECT_FALSE(constraints[1].max_version_exclusive.has_value());
EXPECT_EQ(constraints[1].min_version.value().components(),
std::vector<uint32_t>({128}));
EXPECT_TRUE(constraints[1].permitted_dns_names.empty());
EXPECT_THAT(constraints[2].permitted_dns_names,
testing::ElementsAre("baz.example.com"));
// Other certificates should return nullptr if they are queried for CRS
// constraints. Which test cert used here isn't important as long as it isn't
@ -183,16 +190,25 @@ TEST(TrustStoreChromeTestNoFixture, OverrideConstraints) {
override_constraints;
override_constraints[crypto::SHA256Hash(root3->cert_span())] = {
{std::nullopt, std::nullopt, std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("31"))}};
{std::nullopt,
std::nullopt,
std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("31")),
{}}};
override_constraints[crypto::SHA256Hash(root4->cert_span())] = {
{std::nullopt, std::nullopt, std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("41"))}};
{std::nullopt,
std::nullopt,
std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("41")),
{}}};
override_constraints[crypto::SHA256Hash(root6->cert_span())] = {
{std::nullopt, std::nullopt, std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("61"))}};
{std::nullopt,
std::nullopt,
std::nullopt,
/*max_version_exclusive=*/std::make_optional(base::Version("61")),
{}}};
std::unique_ptr<TrustStoreChrome> trust_store_chrome =
TrustStoreChrome::CreateTrustStoreForTesting(
@ -325,6 +341,7 @@ TEST(TrustStoreChromeTestNoFixture, ParseCommandLineConstraintsErrorHandling) {
EXPECT_FALSE(constraint1.sct_all_after.has_value());
EXPECT_FALSE(constraint1.min_version.has_value());
EXPECT_FALSE(constraint1.max_version_exclusive.has_value());
EXPECT_THAT(constraint1.permitted_dns_names, testing::IsEmpty());
}
{
constexpr uint8_t hash[] = {0xa7, 0xe0, 0xc7, 0x5d, 0x7f, 0x77, 0x2f, 0xcc,
@ -342,6 +359,7 @@ TEST(TrustStoreChromeTestNoFixture, ParseCommandLineConstraintsErrorHandling) {
EXPECT_FALSE(constraint1.sct_all_after.has_value());
EXPECT_FALSE(constraint1.min_version.has_value());
EXPECT_FALSE(constraint1.max_version_exclusive.has_value());
EXPECT_THAT(constraint1.permitted_dns_names, testing::IsEmpty());
}
{
@ -360,6 +378,7 @@ TEST(TrustStoreChromeTestNoFixture, ParseCommandLineConstraintsErrorHandling) {
EXPECT_FALSE(constraint.sct_all_after.has_value());
EXPECT_FALSE(constraint.min_version.has_value());
EXPECT_EQ(constraint.max_version_exclusive, base::Version({2, 3}));
EXPECT_THAT(constraint.permitted_dns_names, testing::IsEmpty());
}
}
@ -392,7 +411,8 @@ TEST(TrustStoreChromeTestNoFixture,
"784ecaa8b9dfcc826547f806f759abd6b4481582fc7e377dc3e6a0a959025126,"
"a7e0c75d7f772fccf26a6ac1f7b0a86a482e2f3d326bc911c95d56ff3d4906d5:"
"sctnotafter=123456,sctallafter=7689,"
"minversion=1.2.3.4,maxversionexclusive=10+"
"minversion=1.2.3.4,maxversionexclusive=10,"
"dns=foo.com,dns=bar.com+"
"a7e0c75d7f772fccf26a6ac1f7b0a86a482e2f3d326bc911c95d56ff3d4906d5,"
"568c8ef6b526d1394bca052ba3e4d1f4d7a8d9c88c55a1a9ab7ca0fae2dc5473:"
"sctallafter=9876543,sctnotafter=1234567890");
@ -415,6 +435,8 @@ TEST(TrustStoreChromeTestNoFixture,
7689);
EXPECT_EQ(constraint1.min_version, base::Version({1, 2, 3, 4}));
EXPECT_EQ(constraint1.max_version_exclusive, base::Version({10}));
EXPECT_THAT(constraint1.permitted_dns_names,
testing::ElementsAre("foo.com", "bar.com"));
}
{
@ -435,6 +457,8 @@ TEST(TrustStoreChromeTestNoFixture,
7689);
EXPECT_EQ(constraint1.min_version, base::Version({1, 2, 3, 4}));
EXPECT_EQ(constraint1.max_version_exclusive, base::Version({10}));
EXPECT_THAT(constraint1.permitted_dns_names,
testing::ElementsAre("foo.com", "bar.com"));
const auto& constraint2 = it->second[1];
ASSERT_TRUE(constraint2.sct_not_after.has_value());
@ -445,6 +469,7 @@ TEST(TrustStoreChromeTestNoFixture,
9876543);
EXPECT_FALSE(constraint2.min_version.has_value());
EXPECT_FALSE(constraint2.max_version_exclusive.has_value());
EXPECT_THAT(constraint2.permitted_dns_names, testing::IsEmpty());
}
{
@ -464,6 +489,7 @@ TEST(TrustStoreChromeTestNoFixture,
9876543);
EXPECT_FALSE(constraint1.min_version.has_value());
EXPECT_FALSE(constraint1.max_version_exclusive.has_value());
EXPECT_THAT(constraint1.permitted_dns_names, testing::IsEmpty());
}
}

@ -32,6 +32,11 @@ message ConstraintSet {
// For example, max_version_exclusive="122" will match any M-121 or earlier
// version, and will not match any M-122 version.
optional string max_version_exclusive = 4;
// All DNS names in the leaf certificate subjectAltNames must fall within the
// subtrees defined by `permitted_dns_names`. The constraints are interpereted
// as described in RFC 5280 section 4.2.1.10.
repeated string permitted_dns_names = 5;
}
message TrustAnchor {

@ -18,9 +18,14 @@ trust_anchors {
constraints: {
sct_not_after_sec: 0x5AF
max_version_exclusive: "125.0.6368.2"
permitted_dns_names: "foo.example.com"
permitted_dns_names: "bar.example.com"
}
constraints: {
sct_all_after_sec: 0x2579
min_version: "128"
}
constraints: {
permitted_dns_names: "baz.example.com"
}
}

@ -186,12 +186,28 @@ bool WriteRootCppFile(const RootStore& root_store,
string_to_write += "};\n";
if (anchor.constraints_size() > 0) {
int constraint_num = 0;
for (const auto& constraint : anchor.constraints()) {
if (constraint.permitted_dns_names_size() > 0) {
base::StringAppendF(&string_to_write,
"constexpr std::string_view "
"kChromeRootConstraint%dNames%d[] = {",
i, constraint_num);
for (const auto& name : constraint.permitted_dns_names()) {
base::StringAppendF(&string_to_write, "\"%s\",", name);
}
string_to_write += "};\n";
}
constraint_num++;
}
base::StringAppendF(&string_to_write,
"constexpr StaticChromeRootCertConstraints "
"kChromeRootConstraints%d[] = {",
i);
std::vector<std::string> constraint_strings;
constraint_num = 0;
for (const auto& constraint : anchor.constraints()) {
std::vector<std::string> constraint_params;
@ -215,8 +231,17 @@ bool WriteRootCppFile(const RootStore& root_store,
? VersionFromString(constraint.max_version_exclusive())
: kNulloptString);
if (constraint.permitted_dns_names_size() > 0) {
constraint_params.push_back(base::StringPrintf(
"kChromeRootConstraint%dNames%d", i, constraint_num));
} else {
constraint_params.push_back("{}");
}
constraint_strings.push_back(
base::StrCat({"{", base::JoinString(constraint_params, ","), "}"}));
constraint_num++;
}
string_to_write += base::JoinString(constraint_strings, ",");