Bug 1871011 - use policy APIs rather than building certificate chains to determine enterprise roots on macOS r=jschanck

Building certificate chains (at startup, no less) is slow. Using the policy
APIs to determine if an enterprise certificate is a trusted root or an
intermediate should be faster.

Differential Revision: https://phabricator.services.mozilla.com/D197740
This commit is contained in:
Dana Keeler
2024-01-25 20:35:06 +00:00
parent 44b1a13a8d
commit 92d2a69249
2 changed files with 206 additions and 39 deletions

View File

@@ -224,23 +224,197 @@ static void GatherEnterpriseCertsWindows(nsTArray<EnterpriseCert>& certs,
#endif // XP_WIN #endif // XP_WIN
#ifdef XP_MACOSX #ifdef XP_MACOSX
enum class CertificateTrustResult {
CanUseAsIntermediate,
CanUseAsTrustAnchor,
DoNotUse,
};
ScopedCFType<CFArrayRef> GetCertificateTrustSettingsInDomain(
const SecCertificateRef certificate, SecTrustSettingsDomain domain) {
CFArrayRef trustSettingsRaw;
OSStatus rv =
SecTrustSettingsCopyTrustSettings(certificate, domain, &trustSettingsRaw);
if (rv != errSecSuccess || !trustSettingsRaw) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" SecTrustSettingsCopyTrustSettings failed (or not found) for "
"domain %" PRIu32,
domain));
return nullptr;
}
ScopedCFType<CFArrayRef> trustSettings(trustSettingsRaw);
return trustSettings;
}
// This function processes trust settings returned by
// SecTrustSettingsCopyTrustSettings. See the documentation at
// https://developer.apple.com/documentation/security/1400261-sectrustsettingscopytrustsetting
// `trustSettings` is an array of CFDictionaryRef. Each dictionary may impose
// a constraint.
CertificateTrustResult ProcessCertificateTrustSettings(
ScopedCFType<CFArrayRef>& trustSettings) {
// If the array is empty, the certificate is a trust anchor.
const CFIndex numTrustDictionaries = CFArrayGetCount(trustSettings.get());
if (numTrustDictionaries == 0) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" empty trust settings -> trust anchor"));
return CertificateTrustResult::CanUseAsTrustAnchor;
}
CertificateTrustResult currentTrustSettings =
CertificateTrustResult::CanUseAsIntermediate;
for (CFIndex i = 0; i < numTrustDictionaries; i++) {
CFDictionaryRef trustDictionary = reinterpret_cast<CFDictionaryRef>(
CFArrayGetValueAtIndex(trustSettings.get(), i));
// kSecTrustSettingsApplication specifies an external application that
// determines the certificate's trust settings.
// kSecTrustSettingsPolicyString appears to be a mechanism like name
// constraints.
// These are not supported, so conservatively assume this certificate is
// distrusted if either are present.
if (CFDictionaryContainsKey(trustDictionary,
kSecTrustSettingsApplication) ||
CFDictionaryContainsKey(trustDictionary,
kSecTrustSettingsPolicyString)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" found unsupported policy -> assuming distrusted"));
return CertificateTrustResult::DoNotUse;
}
// kSecTrustSettingsKeyUsage seems to be essentially the equivalent of the
// x509 keyUsage extension. For parity, we allow
// kSecTrustSettingsKeyUseSignature, kSecTrustSettingsKeyUseSignCert, and
// kSecTrustSettingsKeyUseAny.
if (CFDictionaryContainsKey(trustDictionary, kSecTrustSettingsKeyUsage)) {
CFNumberRef keyUsage = (CFNumberRef)CFDictionaryGetValue(
trustDictionary, kSecTrustSettingsKeyUsage);
int32_t keyUsageValue;
if (!keyUsage ||
CFNumberGetValue(keyUsage, kCFNumberSInt32Type, &keyUsageValue) ||
keyUsageValue < 0) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" no trust settings key usage or couldn't get value"));
return CertificateTrustResult::DoNotUse;
}
switch ((uint64_t)keyUsageValue) {
case kSecTrustSettingsKeyUseSignature: // fall-through
case kSecTrustSettingsKeyUseSignCert: // fall-through
case kSecTrustSettingsKeyUseAny:
break;
default:
return CertificateTrustResult::DoNotUse;
}
}
// If there is a specific policy, ensure that it's for the
// 'kSecPolicyAppleSSL' policy, which is the TLS server auth policy (i.e.
// x509 + domain name checking).
if (CFDictionaryContainsKey(trustDictionary, kSecTrustSettingsPolicy)) {
SecPolicyRef policy = (SecPolicyRef)CFDictionaryGetValue(
trustDictionary, kSecTrustSettingsPolicy);
if (!policy) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" kSecTrustSettingsPolicy present, but null?"));
continue;
}
ScopedCFType<CFDictionaryRef> policyProperties(
SecPolicyCopyProperties(policy));
CFStringRef policyOid = (CFStringRef)CFDictionaryGetValue(
policyProperties.get(), kSecPolicyOid);
if (!CFEqual(policyOid, kSecPolicyAppleSSL)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, (" policy doesn't match"));
continue;
}
}
// By default, the trust setting result value is
// kSecTrustSettingsResultTrustRoot.
int32_t trustSettingsValue = kSecTrustSettingsResultTrustRoot;
if (CFDictionaryContainsKey(trustDictionary, kSecTrustSettingsResult)) {
CFNumberRef trustSetting = (CFNumberRef)CFDictionaryGetValue(
trustDictionary, kSecTrustSettingsResult);
if (!trustSetting || !CFNumberGetValue(trustSetting, kCFNumberSInt32Type,
&trustSettingsValue)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" no trust settings result or couldn't get value"));
continue;
}
}
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
(" trust setting: %d", trustSettingsValue));
if (trustSettingsValue == kSecTrustSettingsResultDeny) {
return CertificateTrustResult::DoNotUse;
}
if (trustSettingsValue == kSecTrustSettingsResultTrustRoot ||
trustSettingsValue == kSecTrustSettingsResultTrustAsRoot) {
currentTrustSettings = CertificateTrustResult::CanUseAsTrustAnchor;
}
}
return currentTrustSettings;
}
CertificateTrustResult GetCertificateTrustResult(
const SecCertificateRef certificate) {
ScopedCFType<CFStringRef> subject(
SecCertificateCopySubjectSummary(certificate));
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("determining trust for '%s'",
CFStringGetCStringPtr(subject.get(), kCFStringEncodingUTF8)));
// There are three trust settings domains: kSecTrustSettingsDomainUser,
// kSecTrustSettingsDomainAdmin, and kSecTrustSettingsDomainSystem. User
// overrides admin and admin overrides system. However, if the given
// certificate has trust settings in the system domain, it shipped with the
// OS, so we don't want to use it.
ScopedCFType<CFArrayRef> systemTrustSettings(
GetCertificateTrustSettingsInDomain(certificate,
kSecTrustSettingsDomainSystem));
if (systemTrustSettings) {
return CertificateTrustResult::DoNotUse;
}
// At this point, if there is no trust information regarding this
// certificate, it can be used as an intermediate.
CertificateTrustResult certificateTrustResult =
CertificateTrustResult::CanUseAsIntermediate;
// Process trust information in the user domain, if any.
ScopedCFType<CFArrayRef> userTrustSettings(
GetCertificateTrustSettingsInDomain(certificate,
kSecTrustSettingsDomainUser));
if (userTrustSettings) {
certificateTrustResult = ProcessCertificateTrustSettings(userTrustSettings);
// If there is definite information one way or another (either indicating
// this is a trusted root or a distrusted certificate), use that
// information.
if (certificateTrustResult !=
CertificateTrustResult::CanUseAsIntermediate) {
return certificateTrustResult;
}
}
// Process trust information in the admin domain, if any.
ScopedCFType<CFArrayRef> adminTrustSettings(
GetCertificateTrustSettingsInDomain(certificate,
kSecTrustSettingsDomainAdmin));
if (adminTrustSettings) {
certificateTrustResult =
ProcessCertificateTrustSettings(adminTrustSettings);
}
// Use whatever result we ended up with.
return certificateTrustResult;
}
OSStatus GatherEnterpriseCertsMacOS(nsTArray<EnterpriseCert>& certs, OSStatus GatherEnterpriseCertsMacOS(nsTArray<EnterpriseCert>& certs,
UniqueSECMODModule& rootsModule) { UniqueSECMODModule& rootsModule) {
// The following builds a search dictionary corresponding to: // The following builds a search dictionary corresponding to:
// { class: "certificate", // { class: "certificate",
// match limit: "match all", // match limit: "match all" }
// policy: "SSL (TLS)",
// only include trusted certificates: true }
// This operates on items that have been added to the keychain and thus gives // This operates on items that have been added to the keychain and thus gives
// us all 3rd party certificates that have been trusted for SSL (TLS), which // us all 3rd party certificates. Unfortunately, if a root that shipped with
// is what we want (thus we don't import built-in root certificates that ship // the OS has had its trust settings changed, it can also be returned from
// with the OS). // this query. Further work (below) filters such certificates out.
const CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecMatchPolicy, const CFStringRef keys[] = {kSecClass, kSecMatchLimit};
kSecMatchTrustedOnly}; const void* values[] = {kSecClassCertificate, kSecMatchLimitAll};
// https://developer.apple.com/documentation/security/1392592-secpolicycreatessl
ScopedCFType<SecPolicyRef> sslPolicy(SecPolicyCreateSSL(true, nullptr));
const void* values[] = {kSecClassCertificate, kSecMatchLimitAll,
sslPolicy.get(), kCFBooleanTrue};
static_assert(ArrayLength(keys) == ArrayLength(values), static_assert(ArrayLength(keys) == ArrayLength(values),
"mismatched SecItemCopyMatching key/value array sizes"); "mismatched SecItemCopyMatching key/value array sizes");
// https://developer.apple.com/documentation/corefoundation/1516782-cfdictionarycreate // https://developer.apple.com/documentation/corefoundation/1516782-cfdictionarycreate
@@ -260,40 +434,31 @@ OSStatus GatherEnterpriseCertsMacOS(nsTArray<EnterpriseCert>& certs,
CFIndex count = CFArrayGetCount(arr.get()); CFIndex count = CFArrayGetCount(arr.get());
uint32_t numImported = 0; uint32_t numImported = 0;
for (CFIndex i = 0; i < count; i++) { for (CFIndex i = 0; i < count; i++) {
const CFTypeRef c = CFArrayGetValueAtIndex(arr.get(), i);
SecTrustRef trust;
rv = SecTrustCreateWithCertificates(c, sslPolicy.get(), &trust);
if (rv != errSecSuccess) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("SecTrustCreateWithCertificates failed"));
continue;
}
ScopedCFType<SecTrustRef> trustHandle(trust);
// Disable AIA chasing to avoid network I/O.
rv = SecTrustSetNetworkFetchAllowed(trustHandle.get(), false);
if (rv != errSecSuccess) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("SecTrustSetNetworkFetchAllowed failed"));
continue;
}
if (!SecTrustEvaluateWithError(trustHandle.get(), nullptr)) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping cert not trusted"));
continue;
}
CFIndex count = SecTrustGetCertificateCount(trustHandle.get());
bool isRoot = count == 1;
// Because we asked for certificates, each CFTypeRef in the array is really // Because we asked for certificates, each CFTypeRef in the array is really
// a SecCertificateRef. // a SecCertificateRef.
const SecCertificateRef s = (const SecCertificateRef)c; const SecCertificateRef certificate =
ScopedCFType<CFDataRef> der(SecCertificateCopyData(s)); (const SecCertificateRef)CFArrayGetValueAtIndex(arr.get(), i);
CertificateTrustResult certificateTrustResult =
GetCertificateTrustResult(certificate);
if (certificateTrustResult == CertificateTrustResult::DoNotUse) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping distrusted cert"));
continue;
}
ScopedCFType<CFDataRef> der(SecCertificateCopyData(certificate));
if (!der) {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("couldn't get bytes of certificate?"));
continue;
}
bool isRoot =
certificateTrustResult == CertificateTrustResult::CanUseAsTrustAnchor;
EnterpriseCert enterpriseCert(CFDataGetBytePtr(der.get()), EnterpriseCert enterpriseCert(CFDataGetBytePtr(der.get()),
CFDataGetLength(der.get()), isRoot); CFDataGetLength(der.get()), isRoot);
if (!enterpriseCert.IsKnownRoot(rootsModule)) { if (!enterpriseCert.IsKnownRoot(rootsModule)) {
certs.AppendElement(std::move(enterpriseCert)); certs.AppendElement(std::move(enterpriseCert));
numImported++; numImported++;
MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
("importing as %s", isRoot ? "root" : "intermediate"));
} else { } else {
MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping known root cert")); MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping known root cert"));
} }

View File

@@ -17,6 +17,8 @@ class ScopedCFType {
public: public:
explicit ScopedCFType(T value) : mValue(value) {} explicit ScopedCFType(T value) : mValue(value) {}
MOZ_IMPLICIT ScopedCFType(decltype(nullptr)) : mValue(nullptr) {}
~ScopedCFType() { ~ScopedCFType() {
if (mValue) { if (mValue) {
CFRelease((CFTypeRef)mValue); CFRelease((CFTypeRef)mValue);