diff --git a/README.md b/README.md index a771cc09..655c1288 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/httplib.h b/httplib.h index b63d969d..e5798f72 100644 --- a/httplib.h +++ b/httplib.h @@ -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(value); + + auto policy_value = + CFDictionaryGetValue(constraint, kSecTrustSettingsPolicy); + if (policy_value) { + auto policy = static_cast(const_cast(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( + CFDictionaryGetValue(constraint, kSecTrustSettingsResult)); + if (number) { + SInt32 number_value = 0; + if (!CFNumberGetValue(number, kCFNumberSInt32Type, &number_value)) { + continue; + } + result = static_cast(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 inline bool enumerate_macos_keychain_certs(Callback cb) { - bool loaded = false; const SecTrustSettingsDomain domains[] = { - kSecTrustSettingsDomainSystem, - kSecTrustSettingsDomainAdmin, kSecTrustSettingsDomainUser, + kSecTrustSettingsDomainAdmin, + kSecTrustSettingsDomainSystem, }; + std::set decided; + std::vector 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(CFDataGetLength(data)))) { - loaded = true; + if (!data) { continue; } + std::string der(reinterpret_cast(CFDataGetBytePtr(data)), + static_cast(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(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( - const_cast(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(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; diff --git a/test/test.cc b/test/test.cc index d63c1bda..dc1917fd 100644 --- a/test/test.cc +++ b/test/test.cc @@ -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 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(*result); + auto number = CFNumberCreate(nullptr, kCFNumberSInt32Type, &number_value); + CFDictionarySetValue(constraint, kSecTrustSettingsResult, number); + CFRelease(number); + } + return constraint; +} + +static SecTrustSettingsResult +ssl_trust_settings_result(std::vector 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