Honour Keychain trust settings when loading macOS system CAs (#2641)

The macOS system CA loader took every certificate that has Trust Settings, in the system, admin or user domain, as a trusted root without reading those settings. A root set to "Never Trust" in Keychain Access, or trusted only for S/MIME or code signing, still verified TLS servers.

The loader now evaluates the settings for the SSL policy. A per-user setting overrides the admin domain, which overrides the system domain, and only TrustRoot and TrustAsRoot are loaded. The OpenSSL path shares the helper that the Mbed TLS and wolfSSL paths already use.
This commit is contained in:
metsw24-max authored and GitHub committed 2026-10-25 21:41:36 -04:00
1 parent 76f84a5bfd
commit ba32b468a4
3 files changed
+203 -40

No files matched your search

+1 -1
View File
@@ -243,7 +243,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
| Platform | Behavior | Disable (compile time) |
| :------- | :------- | :--------------------- |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| macOS | Loads system certs from Keychain, honouring their Trust Settings: a certificate set to "Never Trust", or trusted only for another policy such as S/MIME, is not loaded (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
On Windows, the CryptoAPI check can also be turned off at runtime. The TLS backend then verifies the chain instead, as on other platforms:
+104 -39
View File
@@ -19592,15 +19592,75 @@ inline bool enumerate_windows_system_certs(Callback cb) {
#endif
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
// Enumerate macOS Keychain certificates and call callback with DER data
// The trust a certificate's Trust Settings give it for SSL server
// verification (SecTrustSettings.h). An empty array trusts it as a root.
// Otherwise the first usage-constraints dictionary that applies decides: one
// naming a policy other than SSL, a host name or an application does not
// apply, and a result left out means TrustRoot. Keychain Access writes "Never
// Trust" as kSecTrustSettingsResultDeny.
inline SecTrustSettingsResult
macos_ssl_trust_settings_result(CFArrayRef trust_settings) {
auto count = CFArrayGetCount(trust_settings);
if (count == 0) { return kSecTrustSettingsResultTrustRoot; }
for (CFIndex i = 0; i < count; i++) {
auto value = CFArrayGetValueAtIndex(trust_settings, i);
if (!value || CFGetTypeID(value) != CFDictionaryGetTypeID()) { continue; }
auto constraint = static_cast<CFDictionaryRef>(value);
auto policy_value =
CFDictionaryGetValue(constraint, kSecTrustSettingsPolicy);
if (policy_value) {
auto policy = static_cast<SecPolicyRef>(const_cast<void *>(policy_value));
auto is_ssl = false;
if (auto properties = SecPolicyCopyProperties(policy)) {
auto oid = CFDictionaryGetValue(properties, kSecPolicyOid);
is_ssl = oid && CFEqual(oid, kSecPolicyAppleSSL);
CFRelease(properties);
}
if (!is_ssl) { continue; }
}
if (CFDictionaryContainsKey(constraint, kSecTrustSettingsPolicyString) ||
CFDictionaryContainsKey(constraint, kSecTrustSettingsApplication)) {
continue;
}
auto result = kSecTrustSettingsResultTrustRoot;
auto number = static_cast<CFNumberRef>(
CFDictionaryGetValue(constraint, kSecTrustSettingsResult));
if (number) {
SInt32 number_value = 0;
if (!CFNumberGetValue(number, kCFNumberSInt32Type, &number_value)) {
continue;
}
result = static_cast<SecTrustSettingsResult>(number_value);
}
if (result == kSecTrustSettingsResultTrustRoot ||
result == kSecTrustSettingsResultTrustAsRoot ||
result == kSecTrustSettingsResultDeny) {
return result;
}
}
return kSecTrustSettingsResultUnspecified;
}
// Enumerate the macOS Keychain certificates trusted for SSL and call callback
// with DER data. A certificate with Trust Settings is not trusted just by
// having them: the settings say whether it is, and a per-user setting
// overrides the admin domain's, which overrides the system domain's (whose
// roots are all trusted). So a system root the user set to "Never Trust" is
// left out, as is one trusted only for S/MIME or code signing.
template <typename Callback>
inline bool enumerate_macos_keychain_certs(Callback cb) {
bool loaded = false;
const SecTrustSettingsDomain domains[] = {
kSecTrustSettingsDomainSystem,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainSystem,
};
std::set<std::string> decided;
std::vector<std::string> trusted;
for (auto domain : domains) {
CFArrayRef certs = nullptr;
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
@@ -19613,16 +19673,43 @@ inline bool enumerate_macos_keychain_certs(Callback cb) {
SecCertificateRef cert =
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
CFDataRef data = SecCertificateCopyData(cert);
if (data) {
if (cb(CFDataGetBytePtr(data),
static_cast<size_t>(CFDataGetLength(data)))) {
loaded = true;
if (!data) { continue; }
std::string der(reinterpret_cast<const char *>(CFDataGetBytePtr(data)),
static_cast<size_t>(CFDataGetLength(data)));
CFRelease(data);
// Decided by a domain that overrides this one
if (decided.find(der) != decided.end()) { continue; }
auto result = kSecTrustSettingsResultTrustRoot;
if (domain != kSecTrustSettingsDomainSystem) {
result = kSecTrustSettingsResultUnspecified;
CFArrayRef trust_settings = nullptr;
if (SecTrustSettingsCopyTrustSettings(cert, domain, &trust_settings) ==
errSecSuccess &&
trust_settings) {
result = macos_ssl_trust_settings_result(trust_settings);
}
CFRelease(data);
if (trust_settings) CFRelease(trust_settings);
}
// Neither trusted nor distrusted here; a domain below may decide
if (result == kSecTrustSettingsResultUnspecified) { continue; }
decided.insert(der);
if (result == kSecTrustSettingsResultTrustRoot ||
result == kSecTrustSettingsResultTrustAsRoot) {
trusted.push_back(std::move(der));
}
}
CFRelease(certs);
}
bool loaded = false;
for (const auto &der : trusted) {
if (cb(reinterpret_cast<const unsigned char *>(der.data()), der.size())) {
loaded = true;
}
}
return loaded;
}
#endif
@@ -20056,36 +20143,14 @@ inline bool load_system_certs(ctx_t ctx) {
auto store = SSL_CTX_get_cert_store(ssl_ctx);
if (!store) return false;
bool loaded_any = false;
const SecTrustSettingsDomain domains[] = {
kSecTrustSettingsDomainSystem,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
};
for (auto domain : domains) {
CFArrayRef certs = nullptr;
if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess ||
!certs) {
if (certs) CFRelease(certs);
continue;
}
auto count = CFArrayGetCount(certs);
for (CFIndex i = 0; i < count; i++) {
auto cert = reinterpret_cast<SecCertificateRef>(
const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
CFDataRef der = SecCertificateCopyData(cert);
if (der) {
const unsigned char *data = CFDataGetBytePtr(der);
auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
if (x509) {
if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
X509_free(x509);
}
CFRelease(der);
}
}
CFRelease(certs);
}
auto loaded_any = impl::enumerate_macos_keychain_certs(
[&](const unsigned char *data, size_t len) {
auto x509 = d2i_X509(nullptr, &data, static_cast<long>(len));
if (!x509) { return false; }
auto added = X509_STORE_add_cert(store, x509) == 1;
X509_free(x509);
return added;
});
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
#else
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
+98
View File
@@ -72,6 +72,12 @@ namespace tls {
ca_store_t create_ca_store(const char *pem, size_t len);
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
std::vector<std::string> get_ca_names(ctx_t ctx);
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
namespace impl {
SecTrustSettingsResult
macos_ssl_trust_settings_result(CFArrayRef trust_settings);
} // namespace impl
#endif
} // namespace tls
} // namespace httplib
#endif
@@ -18303,6 +18309,98 @@ TEST(SSLClientTest, EnableSystemCaCustomCaVerifiesLocalServer) {
ASSERT_EQ(StatusCode::OK_200, res->status);
}
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
// A usage-constraints dictionary of a Keychain Trust Settings array, with
// the keys SecTrustSettingsCopyTrustSettings() returns.
static CFDictionaryRef
make_usage_constraint(SecPolicyRef policy, const char *policy_string,
const SecTrustSettingsResult *result) {
auto constraint =
CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
if (policy) {
CFDictionarySetValue(constraint, kSecTrustSettingsPolicy, policy);
}
if (policy_string) {
auto str = CFStringCreateWithCString(nullptr, policy_string,
kCFStringEncodingUTF8);
CFDictionarySetValue(constraint, kSecTrustSettingsPolicyString, str);
CFRelease(str);
}
if (result) {
auto number_value = static_cast<SInt32>(*result);
auto number = CFNumberCreate(nullptr, kCFNumberSInt32Type, &number_value);
CFDictionarySetValue(constraint, kSecTrustSettingsResult, number);
CFRelease(number);
}
return constraint;
}
static SecTrustSettingsResult
ssl_trust_settings_result(std::vector<CFDictionaryRef> constraints) {
auto trust_settings =
CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);
for (auto constraint : constraints) {
CFArrayAppendValue(trust_settings, constraint);
CFRelease(constraint);
}
auto result = tls::impl::macos_ssl_trust_settings_result(trust_settings);
CFRelease(trust_settings);
return result;
}
// The system CA loader takes a Keychain certificate as a trusted root only
// when its Trust Settings say so for SSL, not merely because it has some.
TEST(SSLClientTest, MacOSKeychainTrustSettingsForSSL) {
const auto trust_root = kSecTrustSettingsResultTrustRoot;
const auto trust_as_root = kSecTrustSettingsResultTrustAsRoot;
const auto deny = kSecTrustSettingsResultDeny;
const auto unspecified = kSecTrustSettingsResultUnspecified;
auto ssl_policy = SecPolicyCreateSSL(true, nullptr);
auto x509_policy = SecPolicyCreateBasicX509();
// "Always Trust", which stores no constraint at all
EXPECT_EQ(kSecTrustSettingsResultTrustRoot, ssl_trust_settings_result({}));
EXPECT_EQ(kSecTrustSettingsResultTrustRoot,
ssl_trust_settings_result(
{make_usage_constraint(nullptr, nullptr, nullptr)}));
EXPECT_EQ(kSecTrustSettingsResultTrustRoot,
ssl_trust_settings_result(
{make_usage_constraint(ssl_policy, nullptr, &trust_root)}));
EXPECT_EQ(kSecTrustSettingsResultTrustAsRoot,
ssl_trust_settings_result(
{make_usage_constraint(ssl_policy, nullptr, &trust_as_root)}));
// "Never Trust", for everything or for SSL
EXPECT_EQ(kSecTrustSettingsResultDeny,
ssl_trust_settings_result(
{make_usage_constraint(nullptr, nullptr, &deny)}));
EXPECT_EQ(kSecTrustSettingsResultDeny,
ssl_trust_settings_result(
{make_usage_constraint(ssl_policy, nullptr, &deny)}));
// Trusted for another policy, for one host, or not at all
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
ssl_trust_settings_result(
{make_usage_constraint(x509_policy, nullptr, &trust_root)}));
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
ssl_trust_settings_result({make_usage_constraint(
ssl_policy, "www.example.com", &trust_root)}));
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
ssl_trust_settings_result(
{make_usage_constraint(nullptr, nullptr, &unspecified)}));
// The first constraint that applies to SSL decides
EXPECT_EQ(kSecTrustSettingsResultDeny,
ssl_trust_settings_result(
{make_usage_constraint(x509_policy, nullptr, &trust_root),
make_usage_constraint(nullptr, nullptr, &deny)}));
CFRelease(x509_policy);
CFRelease(ssl_policy);
}
#endif
// Regression test: for IP hosts the Mbed TLS and wolfSSL backends used to
// skip chain verification entirely (only the certificate identity was
// checked), so a server presenting an untrusted certificate with a matching