Browse Source

Support loading system certs from Keychein on MacOS (#1474)

* Support loading system certs from Keychein on MacOS

* review improvements: add deps to meson.build and improve conditional expressions in cmake

* fix tabs

* fix tabs

* review improvements

* fix after review

* additionally load root certs from the system root keychain

* cmake fix

* fix

* small refactoring

* small refactoring

---------

Co-authored-by: Sergey Kazmin <[email protected]>
Sergey Kazmin 2 years ago
parent
commit
6d963fbe8d
4 changed files with 115 additions and 6 deletions
  1. 2 0
      CMakeLists.txt
  2. 103 6
      httplib.h
  3. 3 0
      meson.build
  4. 7 0
      test/Makefile

+ 2 - 0
CMakeLists.txt

@@ -206,6 +206,8 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
 		$<$<PLATFORM_ID:Windows>:ws2_32>
 		$<$<PLATFORM_ID:Windows>:ws2_32>
 		$<$<PLATFORM_ID:Windows>:crypt32>
 		$<$<PLATFORM_ID:Windows>:crypt32>
 		$<$<PLATFORM_ID:Windows>:cryptui>
 		$<$<PLATFORM_ID:Windows>:cryptui>
+		# Needed for API from MacOS Security framework
+		"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>>:-framework CoreFoundation -framework Security>"
 		# Can't put multiple targets in a single generator expression or it bugs out.
 		# Can't put multiple targets in a single generator expression or it bugs out.
 		$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
 		$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
 		$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
 		$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>

+ 103 - 6
httplib.h

@@ -239,7 +239,10 @@ using socket_t = int;
 #pragma comment(lib, "crypt32.lib")
 #pragma comment(lib, "crypt32.lib")
 #pragma comment(lib, "cryptui.lib")
 #pragma comment(lib, "cryptui.lib")
 #endif
 #endif
-#endif //_WIN32
+#elif defined(__APPLE__) // _WIN32
+#include <CoreFoundation/CoreFoundation.h>
+#include <Security/Security.h>
+#endif // __APPLE__
 
 
 #include <openssl/err.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
 #include <openssl/evp.h>
@@ -4388,15 +4391,15 @@ inline std::string SHA_512(const std::string &s) {
 }
 }
 #endif
 #endif
 
 
-#ifdef _WIN32
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
+#ifdef _WIN32
 // NOTE: This code came up with the following stackoverflow post:
 // NOTE: This code came up with the following stackoverflow post:
 // https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
 // https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store
 inline bool load_system_certs_on_windows(X509_STORE *store) {
 inline bool load_system_certs_on_windows(X509_STORE *store) {
   auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT");
   auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT");
-
   if (!hStore) { return false; }
   if (!hStore) { return false; }
 
 
+  auto result = false;
   PCCERT_CONTEXT pContext = NULL;
   PCCERT_CONTEXT pContext = NULL;
   while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
   while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
          nullptr) {
          nullptr) {
@@ -4407,16 +4410,107 @@ inline bool load_system_certs_on_windows(X509_STORE *store) {
     if (x509) {
     if (x509) {
       X509_STORE_add_cert(store, x509);
       X509_STORE_add_cert(store, x509);
       X509_free(x509);
       X509_free(x509);
+      result = true;
     }
     }
   }
   }
 
 
   CertFreeCertificateContext(pContext);
   CertFreeCertificateContext(pContext);
   CertCloseStore(hStore, 0);
   CertCloseStore(hStore, 0);
 
 
+  return result;
+}
+#elif defined(__APPLE__)
+template <typename T>
+using CFObjectPtr =
+    std::unique_ptr<typename std::remove_pointer<T>::type, void (*)(CFTypeRef)>;
+
+inline void cf_object_ptr_deleter(CFTypeRef obj) {
+  if (obj) { CFRelease(obj); }
+}
+
+inline bool retrieve_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
+  CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
+  CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll,
+                        kCFBooleanTrue};
+
+  CFObjectPtr<CFDictionaryRef> query(
+      CFDictionaryCreate(nullptr, reinterpret_cast<const void **>(keys), values,
+                         sizeof(keys) / sizeof(keys[0]),
+                         &kCFTypeDictionaryKeyCallBacks,
+                         &kCFTypeDictionaryValueCallBacks),
+      cf_object_ptr_deleter);
+
+  if (!query) { return false; }
+
+  CFTypeRef security_items = nullptr;
+  if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess ||
+      CFArrayGetTypeID() != CFGetTypeID(security_items)) {
+    return false;
+  }
+
+  certs.reset(reinterpret_cast<CFArrayRef>(security_items));
+  return true;
+}
+
+inline bool retrieve_root_certs_from_keychain(CFObjectPtr<CFArrayRef> &certs) {
+  CFArrayRef root_security_items = nullptr;
+  if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) {
+    return false;
+  }
+
+  certs.reset(root_security_items);
   return true;
   return true;
 }
 }
+
+inline bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) {
+  auto result = false;
+  for (int i = 0; i < CFArrayGetCount(certs); ++i) {
+    const auto cert = reinterpret_cast<const __SecCertificate *>(
+        CFArrayGetValueAtIndex(certs, i));
+
+    if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; }
+
+    CFDataRef cert_data = nullptr;
+    if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) !=
+        errSecSuccess) {
+      continue;
+    }
+
+    CFObjectPtr<CFDataRef> cert_data_ptr(cert_data, cf_object_ptr_deleter);
+
+    auto encoded_cert = static_cast<const unsigned char *>(
+        CFDataGetBytePtr(cert_data_ptr.get()));
+
+    auto x509 =
+        d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get()));
+
+    if (x509) {
+      X509_STORE_add_cert(store, x509);
+      X509_free(x509);
+      result = true;
+    }
+  }
+
+  return result;
+}
+
+inline bool load_system_certs_on_apple(X509_STORE *store) {
+  auto result = false;
+  CFObjectPtr<CFArrayRef> certs(nullptr, cf_object_ptr_deleter);
+  if (retrieve_certs_from_keychain(certs) && certs) {
+    result = add_certs_to_x509_store(certs.get(), store);
+  }
+
+  if (retrieve_root_certs_from_keychain(certs) && certs) {
+    result = add_certs_to_x509_store(certs.get(), store) || result;
+  }
+
+  return result;
+}
+#endif
 #endif
 #endif
 
 
+#ifdef _WIN32
 class WSInit {
 class WSInit {
 public:
 public:
   WSInit() {
   WSInit() {
@@ -7842,11 +7936,14 @@ inline bool SSLClient::load_certs() {
         ret = false;
         ret = false;
       }
       }
     } else {
     } else {
+      auto loaded = false;
 #ifdef _WIN32
 #ifdef _WIN32
-      detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
-#else
-      SSL_CTX_set_default_verify_paths(ctx_);
+      loaded =
+          detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_));
+#elif defined(__APPLE__)
+      loaded = detail::load_system_certs_on_apple(SSL_CTX_get_cert_store(ctx_));
 #endif
 #endif
+      if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); }
     }
     }
   });
   });
 
 

+ 3 - 0
meson.build

@@ -34,6 +34,9 @@ openssl_dep = dependency('openssl', version: '>=1.1.1', required: get_option('cp
 if openssl_dep.found()
 if openssl_dep.found()
   deps += openssl_dep
   deps += openssl_dep
   args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
   args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
+  if host_machine.system() == 'darwin'
+    deps += dependency('appleframeworks', modules: ['CoreFoundation', 'Security'])
+  endif
 endif
 endif
 
 
 zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))
 zlib_dep = dependency('zlib', required: get_option('cpp-httplib_zlib'))

+ 7 - 0
test/Makefile

@@ -8,6 +8,13 @@ OPENSSL_DIR = $(PREFIX)/opt/[email protected]
 #OPENSSL_DIR = $(PREFIX)/opt/openssl@3
 #OPENSSL_DIR = $(PREFIX)/opt/openssl@3
 OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
 OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
 
 
+ifneq ($(OS), Windows_NT)
+	UNAME_S := $(shell uname -s)
+	ifeq ($(UNAME_S), Darwin)
+		OPENSSL_SUPPORT += -framework CoreFoundation -framework Security
+	endif
+endif
+
 ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
 ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
 
 
 BROTLI_DIR = $(PREFIX)/opt/brotli
 BROTLI_DIR = $(PREFIX)/opt/brotli