blob: bba5995eb6cafff29a5a07ceb6f791a1b06b6e59 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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 "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/string_split.h"
#include "base/synchronization/lock.h"
#include "crypto/mac_security_services_lock.h"
#include "net/cert/internal/cert_errors.h"
#include "net/cert/internal/test_helpers.h"
#include "net/cert/pem_tokenizer.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_mac.h"
#include "net/test/test_data_directory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.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,
scoped_refptr<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;
CertErrors errors;
*result = ParsedCertificate::Create(x509_util::CreateCryptoBuffer(der), {},
&errors);
if (!*result) {
return ::testing::AssertionFailure()
<< "ParseCertificate::Create() failed:\n"
<< errors.ToDebugString();
}
return ::testing::AssertionSuccess();
}
// Returns the DER encodings of the SecCertificates in |array|.
std::vector<std::string> SecCertificateArrayAsDER(CFArrayRef array) {
std::vector<std::string> result;
for (CFIndex i = 0, item_count = CFArrayGetCount(array); i < item_count;
++i) {
SecCertificateRef match_cert_handle = reinterpret_cast<SecCertificateRef>(
const_cast<void*>(CFArrayGetValueAtIndex(array, i)));
if (!match_cert_handle) {
ADD_FAILURE() << "null item " << i;
continue;
}
base::ScopedCFTypeRef<CFDataRef> der_data(
SecCertificateCopyData(match_cert_handle));
if (!der_data) {
ADD_FAILURE() << "SecCertificateCopyData error";
continue;
}
result.push_back(std::string(
reinterpret_cast<const char*>(CFDataGetBytePtr(der_data.get())),
CFDataGetLength(der_data.get())));
}
return result;
}
// Returns the DER encodings of the ParsedCertificates in |list|.
std::vector<std::string> ParsedCertificateListAsDER(
ParsedCertificateList list) {
std::vector<std::string> result;
for (const auto& it : list)
result.push_back(it->der_cert().AsString());
return result;
}
} // namespace
// 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(TrustStoreMacTest, 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::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);
TrustStoreMac trust_store(kSecPolicyAppleSSL);
scoped_refptr<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));
base::ScopedCFTypeRef<CFDataRef> normalized_name_b =
TrustStoreMac::GetMacNormalizedIssuer(a_by_b);
ASSERT_TRUE(normalized_name_b);
base::ScopedCFTypeRef<CFDataRef> normalized_name_c =
TrustStoreMac::GetMacNormalizedIssuer(b_by_c);
ASSERT_TRUE(normalized_name_c);
base::ScopedCFTypeRef<CFDataRef> normalized_name_f =
TrustStoreMac::GetMacNormalizedIssuer(b_by_f);
ASSERT_TRUE(normalized_name_f);
base::ScopedCFTypeRef<CFDataRef> normalized_name_d =
TrustStoreMac::GetMacNormalizedIssuer(c_by_d);
ASSERT_TRUE(normalized_name_d);
base::ScopedCFTypeRef<CFDataRef> normalized_name_e =
TrustStoreMac::GetMacNormalizedIssuer(f_by_e);
ASSERT_TRUE(normalized_name_e);
// Test that the matching keychain items are found, even though they aren't
// trusted.
{
base::ScopedCFTypeRef<CFArrayRef> scoped_matching_items =
TrustStoreMac::FindMatchingCertificatesForMacNormalizedSubject(
normalized_name_b.get());
EXPECT_THAT(SecCertificateArrayAsDER(scoped_matching_items),
UnorderedElementsAreArray(
ParsedCertificateListAsDER({b_by_c, b_by_f})));
}
{
base::ScopedCFTypeRef<CFArrayRef> scoped_matching_items =
TrustStoreMac::FindMatchingCertificatesForMacNormalizedSubject(
normalized_name_c.get());
EXPECT_THAT(SecCertificateArrayAsDER(scoped_matching_items),
UnorderedElementsAreArray(
ParsedCertificateListAsDER({c_by_d, c_by_e})));
}
{
base::ScopedCFTypeRef<CFArrayRef> scoped_matching_items =
TrustStoreMac::FindMatchingCertificatesForMacNormalizedSubject(
normalized_name_f.get());
EXPECT_THAT(
SecCertificateArrayAsDER(scoped_matching_items),
UnorderedElementsAreArray(ParsedCertificateListAsDER({f_by_e})));
}
{
base::ScopedCFTypeRef<CFArrayRef> scoped_matching_items =
TrustStoreMac::FindMatchingCertificatesForMacNormalizedSubject(
normalized_name_d.get());
EXPECT_THAT(
SecCertificateArrayAsDER(scoped_matching_items),
UnorderedElementsAreArray(ParsedCertificateListAsDER({d_by_d})));
}
{
base::ScopedCFTypeRef<CFArrayRef> scoped_matching_items =
TrustStoreMac::FindMatchingCertificatesForMacNormalizedSubject(
normalized_name_e.get());
EXPECT_THAT(
SecCertificateArrayAsDER(scoped_matching_items),
UnorderedElementsAreArray(ParsedCertificateListAsDER({e_by_e})));
}
// None of the certs should return any matching TrustAnchors, 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}) {
TrustAnchors matching_anchors;
trust_store.FindTrustAnchorsForCert(cert, &matching_anchors);
EXPECT_EQ(0u, matching_anchors.size());
}
}
// Test against all the certificates in the default keychains. Confirms that
// the computed trust value matches that of SecTrustEvaluate.
TEST(TrustStoreMacTest, 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"
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));
TrustStoreMac trust_store(kSecPolicyAppleX509Basic);
base::ScopedCFTypeRef<SecPolicyRef> sec_policy(SecPolicyCreateBasicX509());
ASSERT_TRUE(sec_policy);
for (const std::string& hash_and_pem : base::SplitStringUsingSubstr(
find_certificate_system_roots_output +
find_certificate_default_search_list_output,
"SHA-1 hash: ", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
std::string::size_type eol_pos = hash_and_pem.find_first_of("\r\n");
ASSERT_NE(std::string::npos, eol_pos);
// Extract the SHA-1 hash of the certificate. This isn't necessary for the
// test, but is a convenient identifier to use in any error messages.
std::string hash_text = hash_and_pem.substr(0, eol_pos);
SCOPED_TRACE(hash_text);
// TODO(mattm): The same cert might exist in both lists, could de-dupe
// before testing?
// Parse the PEM encoded text to DER bytes.
PEMTokenizer pem_tokenizer(hash_and_pem, {kCertificateHeader});
ASSERT_TRUE(pem_tokenizer.GetNext());
std::string cert_der(pem_tokenizer.data());
ASSERT_FALSE(pem_tokenizer.GetNext());
CertErrors errors;
// Note: don't actually need to make a 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.
ParseCertificateOptions options;
// For https://crt.sh/?q=D3EEFBCBBCF49867838626E23BB59CA01E305DB7:
options.allow_invalid_serial_numbers = true;
scoped_refptr<ParsedCertificate> cert = ParsedCertificate::Create(
x509_util::CreateCryptoBuffer(cert_der), options, &errors);
if (!cert) {
LOG(WARNING) << "ParseCertificate::Create " << hash_text << " failed:\n"
<< errors.ToDebugString();
continue;
}
base::ScopedCFTypeRef<SecCertificateRef> cert_handle(
x509_util::CreateSecCertificateFromBytes(cert->der_cert().UnsafeData(),
cert->der_cert().Length()));
if (!cert_handle) {
ADD_FAILURE() << "CreateOSCertHandleFromBytes " << hash_text;
continue;
}
base::ScopedCFTypeRef<CFDataRef> mac_normalized_subject;
{
base::AutoLock lock(crypto::GetMacSecurityServicesLock());
mac_normalized_subject.reset(
SecCertificateCopyNormalizedSubjectContent(cert_handle, nullptr));
}
if (!mac_normalized_subject) {
ADD_FAILURE() << "SecCertificateCopyNormalizedSubjectContent "
<< hash_text;
continue;
}
// Check if this cert is considered a trust anchor by TrustStoreMac.
TrustAnchors trust_anchors;
trust_store.FindTrustAnchorsByMacNormalizedSubject(mac_normalized_subject,
&trust_anchors);
bool is_trust_anchor = false;
for (const auto& anchor : trust_anchors) {
ASSERT_TRUE(anchor->cert());
if (anchor->cert()->der_cert() == cert->der_cert())
is_trust_anchor = true;
}
// Check if this cert is considered a trust anchor by the OS.
base::ScopedCFTypeRef<SecTrustRef> trust;
{
base::AutoLock lock(crypto::GetMacSecurityServicesLock());
ASSERT_EQ(noErr,
SecTrustCreateWithCertificates(cert_handle, sec_policy,
trust.InitializeInto()));
ASSERT_EQ(noErr,
SecTrustSetOptions(
trust,
kSecTrustOptionLeafIsCA | kSecTrustOptionAllowExpiredRoot));
SecTrustResultType trust_result;
ASSERT_EQ(noErr, SecTrustEvaluate(trust, &trust_result));
bool expected_trust_anchor =
((trust_result == kSecTrustResultProceed) ||
(trust_result == kSecTrustResultUnspecified)) &&
(SecTrustGetCertificateCount(trust) == 1);
EXPECT_EQ(expected_trust_anchor, is_trust_anchor);
}
}
}
} // namespace net