mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-26 12:24:46 +00:00
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:
1 parent
76f84a5bfd
commit
ba32b468a4
3 files changed
+203
-40
No files matched your search
@@ -243,7 +243,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
|
|||||||
|
|
||||||
| Platform | Behavior | Disable (compile time) |
|
| 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` |
|
| 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:
|
On Windows, the CryptoAPI check can also be turned off at runtime. The TLS backend then verifies the chain instead, as on other platforms:
|
||||||
|
|||||||
@@ -19592,15 +19592,75 @@ inline bool enumerate_windows_system_certs(Callback cb) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#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>
|
template <typename Callback>
|
||||||
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
||||||
bool loaded = false;
|
|
||||||
const SecTrustSettingsDomain domains[] = {
|
const SecTrustSettingsDomain domains[] = {
|
||||||
kSecTrustSettingsDomainSystem,
|
|
||||||
kSecTrustSettingsDomainAdmin,
|
|
||||||
kSecTrustSettingsDomainUser,
|
kSecTrustSettingsDomainUser,
|
||||||
|
kSecTrustSettingsDomainAdmin,
|
||||||
|
kSecTrustSettingsDomainSystem,
|
||||||
};
|
};
|
||||||
|
std::set<std::string> decided;
|
||||||
|
std::vector<std::string> trusted;
|
||||||
for (auto domain : domains) {
|
for (auto domain : domains) {
|
||||||
CFArrayRef certs = nullptr;
|
CFArrayRef certs = nullptr;
|
||||||
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
||||||
@@ -19613,16 +19673,43 @@ inline bool enumerate_macos_keychain_certs(Callback cb) {
|
|||||||
SecCertificateRef cert =
|
SecCertificateRef cert =
|
||||||
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
|
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
|
||||||
CFDataRef data = SecCertificateCopyData(cert);
|
CFDataRef data = SecCertificateCopyData(cert);
|
||||||
if (data) {
|
if (!data) { continue; }
|
||||||
if (cb(CFDataGetBytePtr(data),
|
std::string der(reinterpret_cast<const char *>(CFDataGetBytePtr(data)),
|
||||||
static_cast<size_t>(CFDataGetLength(data)))) {
|
static_cast<size_t>(CFDataGetLength(data)));
|
||||||
loaded = true;
|
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);
|
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;
|
return loaded;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -20056,36 +20143,14 @@ inline bool load_system_certs(ctx_t ctx) {
|
|||||||
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
||||||
if (!store) return false;
|
if (!store) return false;
|
||||||
|
|
||||||
bool loaded_any = false;
|
auto loaded_any = impl::enumerate_macos_keychain_certs(
|
||||||
const SecTrustSettingsDomain domains[] = {
|
[&](const unsigned char *data, size_t len) {
|
||||||
kSecTrustSettingsDomainSystem,
|
auto x509 = d2i_X509(nullptr, &data, static_cast<long>(len));
|
||||||
kSecTrustSettingsDomainAdmin,
|
if (!x509) { return false; }
|
||||||
kSecTrustSettingsDomainUser,
|
auto added = X509_STORE_add_cert(store, x509) == 1;
|
||||||
};
|
X509_free(x509);
|
||||||
for (auto domain : domains) {
|
return added;
|
||||||
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);
|
|
||||||
}
|
|
||||||
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
#else
|
#else
|
||||||
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ namespace tls {
|
|||||||
ca_store_t create_ca_store(const char *pem, size_t len);
|
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||||
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
||||||
std::vector<std::string> get_ca_names(ctx_t ctx);
|
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 tls
|
||||||
} // namespace httplib
|
} // namespace httplib
|
||||||
#endif
|
#endif
|
||||||
@@ -18303,6 +18309,98 @@ TEST(SSLClientTest, EnableSystemCaCustomCaVerifiesLocalServer) {
|
|||||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
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
|
// Regression test: for IP hosts the Mbed TLS and wolfSSL backends used to
|
||||||
// skip chain verification entirely (only the certificate identity was
|
// skip chain verification entirely (only the certificate identity was
|
||||||
// checked), so a server presenting an untrusted certificate with a matching
|
// checked), so a server presenting an untrusted certificate with a matching
|
||||||
|
|||||||
Reference in new issue
Block a user