From 1ff0c8588d7a2c1baa251e8d615e86301526d8ec Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 23 May 2026 08:39:45 -0400 Subject: [PATCH] Fix iOS build break and modernize macOS Keychain cert loading (#2455) * Replace deprecated SecTrustCopyAnchorCertificates on macOS SecTrustCopyAnchorCertificates was deprecated in macOS 13. Switch to SecTrustSettingsCopyCertificates, iterating over the System, Admin, and User trust domains to retain equivalent coverage of anchor certificates. * Restrict Keychain cert loading to macOS TARGET_OS_MAC is true on all Apple platforms including iOS, tvOS, and watchOS, which caused the keychain enumeration path to be compiled on iOS where SecTrustSettingsCopyCertificates is unavailable. Narrow the auto-enable and the Security.h include guards to TARGET_OS_OSX, and emit an explicit #error when the user defines CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on a non-macOS Apple platform, directing them to use set_ca_cert_path() with a bundled CA file. Addresses the iOS build break reported in #2454. * Add iOS header parse check to CI Run a cross-compile syntax check against the iOS SDK to catch accidental use of macOS-only APIs or guards (e.g. TARGET_OS_MAC vs TARGET_OS_OSX) that would silently break iOS builds. Also verify that defining CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on iOS fires the expected #error. iOS is not officially supported as a runtime target; this job only guarantees the header stays parse-clean on iOS toolchains. --- .github/workflows/test.yaml | 48 ++++++++++++++++++++++ httplib.h | 79 +++++++++++++++++++++++++------------ 2 files changed, 101 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 38787cc..d0e41c3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -250,6 +250,54 @@ jobs: - name: build and run ThreadPool test run: cd test && make test_thread_pool && ./test_thread_pool + ios-parse-check: + runs-on: macos-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true') + name: ios header parse check (not officially supported) + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install OpenSSL headers + run: brew install openssl@3 + - name: verify header parses on iOS target + run: | + IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path) + OPENSSL_INC=$(brew --prefix openssl@3)/include + echo "Using iOS SDK: $IOS_SDK" + echo '#include "httplib.h"' | clang++ \ + -isysroot "$IOS_SDK" \ + -target arm64-apple-ios16.0 \ + -std=c++11 \ + -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -I"$OPENSSL_INC" \ + -I. -Wall -Wextra \ + -fsyntax-only -x c++ - + - name: verify CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is rejected on iOS + run: | + IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path) + OPENSSL_INC=$(brew --prefix openssl@3)/include + out=$(echo '#include "httplib.h"' | clang++ \ + -isysroot "$IOS_SDK" \ + -target arm64-apple-ios16.0 \ + -std=c++11 \ + -DCPPHTTPLIB_OPENSSL_SUPPORT \ + -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN \ + -I"$OPENSSL_INC" \ + -I. \ + -fsyntax-only -x c++ - 2>&1 || true) + if echo "$out" | grep -q "only supported on macOS"; then + echo "OK: #error fired as expected" + else + echo "FAIL: expected #error did not fire" + echo "--- compiler output ---" + echo "$out" + exit 1 + fi + windows: runs-on: windows-latest if: > diff --git a/httplib.h b/httplib.h index 7c924e6..2af1f50 100644 --- a/httplib.h +++ b/httplib.h @@ -339,16 +339,26 @@ using socket_t = int; #include // On macOS with a TLS backend, enable Keychain root certificates by default -// unless the user explicitly opts out. +// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since +// the SecTrustSettings APIs used to enumerate anchor certificates are macOS +// only; on those platforms the user must provide a CA bundle explicitly. #if defined(__APPLE__) && defined(__clang__) && \ !defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \ (defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \ defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \ defined(CPPHTTPLIB_WOLFSSL_SUPPORT)) +#if TARGET_OS_OSX #ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN #define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN #endif #endif +#endif + +#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \ + defined(__APPLE__) && !TARGET_OS_OSX +#error \ + "CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is only supported on macOS. On iOS/tvOS/watchOS, supply a CA bundle via set_ca_cert_path()." +#endif // On Windows, enable Schannel certificate verification by default // unless the user explicitly opts out. @@ -382,7 +392,7 @@ using socket_t = int; #endif // _WIN32 #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_MAC +#if TARGET_OS_OSX #include #endif #endif @@ -430,7 +440,7 @@ using socket_t = int; #endif #endif // _WIN32 #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_MAC +#if TARGET_OS_OSX #include #endif #endif @@ -473,7 +483,7 @@ using socket_t = int; #endif #endif // _WIN32 #ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_MAC +#if TARGET_OS_OSX #include #endif #endif @@ -16143,9 +16153,18 @@ inline bool enumerate_windows_system_certs(Callback cb) { template inline bool enumerate_macos_keychain_certs(Callback cb) { bool loaded = false; - CFArrayRef certs = nullptr; - OSStatus status = SecTrustCopyAnchorCertificates(&certs); - if (status == errSecSuccess && certs) { + const SecTrustSettingsDomain domains[] = { + kSecTrustSettingsDomainSystem, + kSecTrustSettingsDomainAdmin, + kSecTrustSettingsDomainUser, + }; + for (auto domain : domains) { + CFArrayRef certs = nullptr; + OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs); + if (status != errSecSuccess || !certs) { + if (certs) CFRelease(certs); + continue; + } CFIndex count = CFArrayGetCount(certs); for (CFIndex i = 0; i < count; i++) { SecCertificateRef cert = @@ -16508,28 +16527,36 @@ inline bool load_system_certs(ctx_t ctx) { auto store = SSL_CTX_get_cert_store(ssl_ctx); if (!store) return false; - CFArrayRef certs = nullptr; - if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) { - return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; - } - bool loaded_any = false; - 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); + 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); } - CFRelease(certs); return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; #else return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;