blob: d2ce4a36b26743eecd855a71d7aee39dcba6e151 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/cert/internal/trust_store_mac.h"
#include <algorithm>
#include <set>
#include "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/synchronization/lock.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "crypto/mac_security_services_lock.h"
#include "crypto/sha2.h"
#include "net/base/features.h"
#include "net/cert/internal/test_helpers.h"
#include "net/cert/internal/trust_store_features.h"
#include "net/cert/test_keychain_search_list_mac.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util.h"
#include "net/cert/x509_util_apple.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"
#include "third_party/boringssl/src/pki/pem.h"
#include "third_party/boringssl/src/pki/trust_store.h"
using ::testing::UnorderedElementsAreArray;
namespace net {
namespace {
// The PEM block header used for DER certificates
const char kCertificateHeader[] = "CERTIFICATE";
// Parses a PEM encoded certificate from |file_name| and stores in |result|.
::testing::AssertionResult ReadTestCert(
const std::string& file_name,
std::shared_ptr<const bssl::ParsedCertificate>* result) {
std::string der;
const PemBlockMapping mappings[] = {
{kCertificateHeader, &der},
};
::testing::AssertionResult r = ReadTestDataFromPemFile(
"net/data/ssl/certificates/" + file_name, mappings);
if (!r)
return r;
bssl::CertErrors errors;
*result = bssl::ParsedCertificate::Create(x509_util::CreateCryptoBuffer(der),
{}, &errors);
if (!*result) {
return ::testing::AssertionFailure()
<< "bssl::ParseCertificate::Create() failed:\n"
<< errors.ToDebugString();
}
return ::testing::AssertionSuccess();
}
// Returns the DER encodings of the ParsedCertificates in |list|.
std::vector<std::string> ParsedCertificateListAsDER(
bssl::ParsedCertificateList list) {
std::vector<std::string> result;
for (const auto& it : list)
result.push_back(it->der_cert().AsString());
return result;
}
std::set<std::string> ParseFindCertificateOutputToDerCerts(std::string output) {
std::set<std::string> certs;
for (const std::string& hash_and_pem_partial : base::SplitStringUsingSubstr(
output, "-----END CERTIFICATE-----", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY)) {
// Re-add the PEM ending mark, since SplitStringUsingSubstr eats it.
const std::string hash_and_pem =
hash_and_pem_partial + "\n-----END CERTIFICATE-----\n";
// Parse the PEM encoded text to DER bytes.
bssl::PEMTokenizer pem_tokenizer(hash_and_pem, {kCertificateHeader});
if (!pem_tokenizer.GetNext()) {
ADD_FAILURE() << "!pem_tokenizer.GetNext()";
continue;
}
std::string cert_der(pem_tokenizer.data());
EXPECT_FALSE(pem_tokenizer.GetNext());
certs.insert(cert_der);
}
return certs;
}
const char* TrustImplTypeToString(TrustStoreMac::TrustImplType t) {
switch (t) {
case TrustStoreMac::TrustImplType::kSimple:
return "Simple";
case TrustStoreMac::TrustImplType::kDomainCacheFullCerts:
return "DomainCacheFullCerts";
case TrustStoreMac::TrustImplType::kKeychainCacheFullCerts:
return "KeychainCacheFullCerts";
case TrustStoreMac::TrustImplType::kUnknown:
return "Unknown";
}
}
} // namespace
class TrustStoreMacImplTest
: public testing::TestWithParam<
std::tuple<TrustStoreMac::TrustImplType, bool, bool>> {
public:
TrustStoreMacImplTest()
: scoped_enforce_local_anchor_constraints_(
ExpectedEnforceLocalAnchorConstraintsEnabled()) {
if (ExpectedTrustedLeafSupportEnabled()) {
feature_list_.InitAndEnableFeature(
features::kTrustStoreTrustedLeafSupport);
} else {
feature_list_.InitAndDisableFeature(
features::kTrustStoreTrustedLeafSupport);
}
}
TrustStoreMac::TrustImplType GetImplParam() const {
return std::get<0>(GetParam());
}
bool ExpectedTrustedLeafSupportEnabled() const {
return std::get<1>(GetParam());
}
bool ExpectedEnforceLocalAnchorConstraintsEnabled() const {
return std::get<2>(GetParam());
}
bssl::CertificateTrust ExpectedTrustForAnchor() const {
bssl::CertificateTrust trust;
if (ExpectedTrustedLeafSupportEnabled()) {
trust = bssl::CertificateTrust::ForTrustAnchorOrLeaf()
.WithEnforceAnchorExpiry();
} else {
trust =
bssl::CertificateTrust::ForTrustAnchor().WithEnforceAnchorExpiry();
}
if (ExpectedEnforceLocalAnchorConstraintsEnabled()) {
trust = trust.WithEnforceAnchorConstraints()
.WithRequireAnchorBasicConstraints();
}
return trust;
}
private:
base::test::ScopedFeatureList feature_list_;
ScopedLocalAnchorConstraintsEnforcementForTesting
scoped_enforce_local_anchor_constraints_;
};
// Much of the Keychain API was marked deprecated as of the macOS 13 SDK.
// Removal of its use is tracked in https://crbug.com/1348251 but deprecation
// warnings are disabled in the meanwhile.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// Test the trust store using known test certificates in a keychain. Tests
// that issuer searching returns the expected certificates, and that none of
// the certificates are trusted.
TEST_P(TrustStoreMacImplTest, MultiRootNotTrusted) {
std::unique_ptr<TestKeychainSearchList> test_keychain_search_list(
TestKeychainSearchList::Create());
ASSERT_TRUE(test_keychain_search_list);
base::FilePath keychain_path(
GetTestCertsDirectory().AppendASCII("multi-root.keychain"));
// SecKeychainOpen does not fail if the file doesn't exist, so assert it here
// for easier debugging.
ASSERT_TRUE(base::PathExists(keychain_path));
base::apple::ScopedCFTypeRef<SecKeychainRef> keychain;
OSStatus status = SecKeychainOpen(keychain_path.MaybeAsASCII().c_str(),
keychain.InitializeInto());
ASSERT_EQ(errSecSuccess, status);
ASSERT_TRUE(keychain);
test_keychain_search_list->AddKeychain(keychain.get());
#pragma clang diagnostic pop
const TrustStoreMac::TrustImplType trust_impl = GetImplParam();
TrustStoreMac trust_store(kSecPolicyAppleSSL, trust_impl);
std::shared_ptr<const bssl::ParsedCertificate> a_by_b, b_by_c, b_by_f, c_by_d,
c_by_e, f_by_e, d_by_d, e_by_e;
ASSERT_TRUE(ReadTestCert("multi-root-A-by-B.pem", &a_by_b));
ASSERT_TRUE(ReadTestCert("multi-root-B-by-C.pem", &b_by_c));
ASSERT_TRUE(ReadTestCert("multi-root-B-by-F.pem", &b_by_f));
ASSERT_TRUE(ReadTestCert("multi-root-C-by-D.pem", &c_by_d));
ASSERT_TRUE(ReadTestCert("multi-root-C-by-E.pem", &c_by_e));
ASSERT_TRUE(ReadTestCert("multi-root-F-by-E.pem", &f_by_e));
ASSERT_TRUE(ReadTestCert("multi-root-D-by-D.pem", &d_by_d));
ASSERT_TRUE(ReadTestCert("multi-root-E-by-E.pem", &e_by_e));
// Test that the untrusted keychain certs would be found during issuer
// searching.
{
bssl::ParsedCertificateList found_issuers;
trust_store.SyncGetIssuersOf(a_by_b.get(), &found_issuers);
EXPECT_THAT(ParsedCertificateListAsDER(found_issuers),
UnorderedElementsAreArray(
ParsedCertificateListAsDER({b_by_c, b_by_f})));
}
{
bssl::ParsedCertificateList found_issuers;
trust_store.SyncGetIssuersOf(b_by_c.get(), &found_issuers);
EXPECT_THAT(ParsedCertificateListAsDER(found_issuers),
UnorderedElementsAreArray(
ParsedCertificateListAsDER({c_by_d, c_by_e})));
}
{
bssl::ParsedCertificateList found_issuers;
trust_store.SyncGetIssuersOf(b_by_f.get(), &found_issuers);
EXPECT_THAT(
ParsedCertificateListAsDER(found_issuers),
UnorderedElementsAreArray(ParsedCertificateListAsDER({f_by_e})));
}
{
bssl::ParsedCertificateList found_issuers;
trust_store.SyncGetIssuersOf(c_by_d.get(), &found_issuers);
EXPECT_THAT(
ParsedCertificateListAsDER(found_issuers),
UnorderedElementsAreArray(ParsedCertificateListAsDER({d_by_d})));
}
{
bssl::ParsedCertificateList found_issuers;
trust_store.SyncGetIssuersOf(f_by_e.get(), &found_issuers);
EXPECT_THAT(
ParsedCertificateListAsDER(found_issuers),
UnorderedElementsAreArray(ParsedCertificateListAsDER({e_by_e})));
}
// Verify that none of the added certificates are considered trusted (since
// the test certs in the keychain aren't trusted, unless someone manually
// added and trusted the test certs on the machine the test is being run on).
for (const auto& cert :
{a_by_b, b_by_c, b_by_f, c_by_d, c_by_e, f_by_e, d_by_d, e_by_e}) {
bssl::CertificateTrust trust = trust_store.GetTrust(cert.get());
EXPECT_EQ(bssl::CertificateTrust::ForUnspecified().ToDebugString(),
trust.ToDebugString());
}
}
// Test against all the certificates in the default keychains. Confirms that
// the computed trust value matches that of SecTrustEvaluateWithError.
TEST_P(TrustStoreMacImplTest, SystemCerts) {
// Get the list of all certificates in the user & system keychains.
// This may include both trusted and untrusted certificates.
//
// The output contains zero or more repetitions of:
// "SHA-1 hash: <hash>\n<PEM encoded cert>\n"
// Starting with macOS 10.15, it includes both SHA-256 and SHA-1 hashes:
// "SHA-256 hash: <hash>\nSHA-1 hash: <hash>\n<PEM encoded cert>\n"
std::string find_certificate_default_search_list_output;
ASSERT_TRUE(
base::GetAppOutput({"security", "find-certificate", "-a", "-p", "-Z"},
&find_certificate_default_search_list_output));
// Get the list of all certificates in the system roots keychain.
// (Same details as above.)
std::string find_certificate_system_roots_output;
ASSERT_TRUE(base::GetAppOutput(
{"security", "find-certificate", "-a", "-p", "-Z",
"/System/Library/Keychains/SystemRootCertificates.keychain"},
&find_certificate_system_roots_output));
std::set<std::string> find_certificate_default_search_list_certs =
ParseFindCertificateOutputToDerCerts(
find_certificate_default_search_list_output);
std::set<std::string> find_certificate_system_roots_certs =
ParseFindCertificateOutputToDerCerts(
find_certificate_system_roots_output);
const TrustStoreMac::TrustImplType trust_impl = GetImplParam();
base::HistogramTester histogram_tester;
TrustStoreMac trust_store(kSecPolicyAppleX509Basic, trust_impl);
base::apple::ScopedCFTypeRef<SecPolicyRef> sec_policy(
SecPolicyCreateBasicX509());
ASSERT_TRUE(sec_policy);
std::vector<std::string> all_certs;
std::set_union(find_certificate_default_search_list_certs.begin(),
find_certificate_default_search_list_certs.end(),
find_certificate_system_roots_certs.begin(),
find_certificate_system_roots_certs.end(),
std::back_inserter(all_certs));
for (const std::string& cert_der : all_certs) {
std::string hash = crypto::SHA256HashString(cert_der);
std::string hash_text = base::HexEncode(hash.data(), hash.size());
SCOPED_TRACE(hash_text);
bssl::CertErrors errors;
// Note: don't actually need to make a bssl::ParsedCertificate here, just
// need the DER bytes. But parsing it here ensures the test can skip any
// certs that won't be returned due to parsing failures inside
// TrustStoreMac. The parsing options set here need to match the ones used
// in trust_store_mac.cc.
bssl::ParseCertificateOptions options;
// For https://crt.sh/?q=D3EEFBCBBCF49867838626E23BB59CA01E305DB7:
options.allow_invalid_serial_numbers = true;
std::shared_ptr<const bssl::ParsedCertificate> cert =
bssl::ParsedCertificate::Create(x509_util::CreateCryptoBuffer(cert_der),
options, &errors);
if (!cert) {
LOG(WARNING) << "bssl::ParseCertificate::Create " << hash_text
<< " failed:\n"
<< errors.ToDebugString();
continue;
}
base::apple::ScopedCFTypeRef<SecCertificateRef> cert_handle(
x509_util::CreateSecCertificateFromBytes(cert->der_cert().UnsafeData(),
cert->der_cert().Length()));
if (!cert_handle) {
ADD_FAILURE() << "CreateCertBufferFromBytes " << hash_text;
continue;
}
// Check if this cert is considered a trust anchor by TrustStoreMac.
bssl::CertificateTrust cert_trust = trust_store.GetTrust(cert.get());
bool is_trusted = cert_trust.IsTrustAnchor() || cert_trust.IsTrustLeaf();
if (is_trusted) {
EXPECT_EQ(ExpectedTrustForAnchor().ToDebugString(),
cert_trust.ToDebugString());
}
// Check if this cert is considered a trust anchor by the OS.
base::apple::ScopedCFTypeRef<SecTrustRef> trust;
{
base::AutoLock lock(crypto::GetMacSecurityServicesLock());
ASSERT_EQ(noErr, SecTrustCreateWithCertificates(cert_handle.get(),
sec_policy.get(),
trust.InitializeInto()));
ASSERT_EQ(noErr, SecTrustSetOptions(trust.get(),
kSecTrustOptionLeafIsCA |
kSecTrustOptionAllowExpired |
kSecTrustOptionAllowExpiredRoot));
if (find_certificate_default_search_list_certs.count(cert_der) &&
find_certificate_system_roots_certs.count(cert_der)) {
// If the same certificate is present in both the System and User/Admin
// domains, and TrustStoreMac is only using trust settings from
// User/Admin, then it's not possible for this test to know whether the
// result from SecTrustEvaluate should match the TrustStoreMac result.
// Just ignore such certificates.
} else if (!find_certificate_default_search_list_certs.count(cert_der)) {
// Cert is only in the system domain. It should be untrusted.
EXPECT_FALSE(is_trusted);
} else {
bool trusted = SecTrustEvaluateWithError(trust.get(), nullptr);
bool expected_trust_anchor =
trusted && (SecTrustGetCertificateCount(trust.get()) == 1);
EXPECT_EQ(expected_trust_anchor, is_trusted);
}
}
// Call GetTrust again on the same cert. This should exercise the code
// that checks the trust value for a cert which has already been cached.
bssl::CertificateTrust cert_trust2 = trust_store.GetTrust(cert.get());
EXPECT_EQ(cert_trust.ToDebugString(), cert_trust2.ToDebugString());
}
// Since this is testing the actual platform certs and trust settings, we
// don't know what values the histograms should be, so just verify that the
// histogram is recorded (or not) depending on the requested trust impl.
{
// Histograms only logged by DomainCacheFullCerts impl:
const int expected_count =
(trust_impl == TrustStoreMac::TrustImplType::kDomainCacheFullCerts) ? 1
: 0;
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacTrustDomainCertCount.User", expected_count);
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacTrustDomainCertCount.Admin", expected_count);
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacTrustDomainCacheInitTime", expected_count);
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacKeychainCerts.IntermediateCacheInitTime",
expected_count);
}
{
// Histograms only logged by KeychainCacheFullCerts impl:
const int expected_count =
(trust_impl == TrustStoreMac::TrustImplType::kKeychainCacheFullCerts)
? 1
: 0;
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacKeychainCerts.TrustCount", expected_count);
}
{
// Histograms logged by both DomainCacheFullCerts and KeychainCacheFullCerts
// impls:
const int expected_count =
(trust_impl == TrustStoreMac::TrustImplType::kDomainCacheFullCerts ||
trust_impl == TrustStoreMac::TrustImplType::kKeychainCacheFullCerts)
? 1
: 0;
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacKeychainCerts.IntermediateCount", expected_count);
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacKeychainCerts.TotalCount", expected_count);
histogram_tester.ExpectTotalCount(
"Net.CertVerifier.MacTrustImplCacheInitTime", expected_count);
}
}
INSTANTIATE_TEST_SUITE_P(
Impl,
TrustStoreMacImplTest,
testing::Combine(
testing::Values(TrustStoreMac::TrustImplType::kSimple,
TrustStoreMac::TrustImplType::kDomainCacheFullCerts,
TrustStoreMac::TrustImplType::kKeychainCacheFullCerts),
testing::Bool(),
testing::Bool()),
[](const testing::TestParamInfo<TrustStoreMacImplTest::ParamType>& info) {
return base::StrCat(
{TrustImplTypeToString(std::get<0>(info.param)),
std::get<1>(info.param) ? "TrustedLeafSupported" : "TrustAnchorOnly",
std::get<2>(info.param) ? "EnforceLocalAnchorConstraints"
: "NoLocalAnchorConstraints"});
});
} // namespace net