Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds macOS Keychain certs to default CA store #6588

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 312 additions & 1 deletion packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,117 @@ void *sni_find(void *sni, const char *hostname);
#include <wolfssl/openssl/dh.h>
#endif

#if defined(__APPLE__) && defined(__MACH__)

#include <dlfcn.h>

// CoreFoundation Types, Enums & Macros
#define CFSTR(cStr) cf_funcs.__CFStringMakeConstantString("" cStr "")

typedef int32_t OSStatus;
typedef unsigned char Boolean;
typedef signed long CFIndex;
typedef void *CFArrayRef;
typedef void *CFMutableArrayRef;
typedef void *CFDataRef;
typedef void *CFStringRef;
typedef void *CFDictionaryRef;
typedef void *CFNumberRef;

typedef enum : CFIndex {
kCFCompareLessThan = -1L,
kCFCompareEqualTo = 0,
kCFCompareGreaterThan = 1
} CFComparisonResult;

typedef enum : CFIndex {
kCFNumberSInt32Type = 3L,
} CFNumberType;

// Security Framework Types, Enums & Macros
#define kSecTrustSettingsResult CFSTR("kSecTrustSettingsResult")

typedef void *SecCertificateRef;

typedef enum : uint32_t {
kSecTrustSettingsDomainUser = 0,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainSystem
} SecTrustSettingsDomain;

enum : OSStatus {
errSecSuccess = 0,
errSecNoTrustSettings = -25263,
errSecItemNotFound = -25300,
};

typedef enum : uint32_t {
kSecTrustSettingsResultInvalid = 0, /* Never valid in a Trust Settings array or
* in an API call. */
kSecTrustSettingsResultTrustRoot, /* Root cert is explicitly trusted */
kSecTrustSettingsResultTrustAsRoot, /* Non-root cert is explicitly trusted */
kSecTrustSettingsResultDeny, /* Cert is explicitly distrusted */
kSecTrustSettingsResultUnspecified /* Neither trusted nor distrusted; evaluation
* proceeds as usual */
} SecTrustSettingsResult;

// CoreFoundation Functions
struct {
void *handle;

void (*CFRelease)(void *cf);

CFMutableArrayRef (*CFArrayCreateMutable)(void *, CFIndex, void *);

const void *(*CFArrayGetValueAtIndex)(CFArrayRef, CFIndex);

CFIndex (*CFArrayGetCount)(CFArrayRef);

void (*CFArrayAppendValue)(CFMutableArrayRef, const void *);

const uint8_t *(*CFDataGetBytePtr)(CFDataRef);

CFIndex (*CFDataGetLength)(CFDataRef);

CFStringRef (*__CFStringMakeConstantString)(const char *);

CFComparisonResult (*CFStringCompare)(CFStringRef, CFStringRef, unsigned long);

Boolean (*CFDictionaryGetValueIfPresent)(CFDictionaryRef, const void *, const void **);

const void *(*CFDictionaryGetValue)(CFDictionaryRef, const void *);

Boolean (*CFNumberGetValue)(CFNumberRef, CFNumberType, void *);
} cf_funcs;

// Security Framework Functions
struct {
void *handle;

CFDataRef (*SecCertificateCopyData)(SecCertificateRef);

OSStatus (*SecTrustSettingsCopyCertificates)(SecTrustSettingsDomain, CFArrayRef *);

OSStatus (*SecTrustSettingsCopyTrustSettings)(SecCertificateRef, SecTrustSettingsDomain, CFArrayRef *);
} sec_funcs;

#endif

#include "./root_certs.h"
#include <stdatomic.h>

// Embedded root certs
static const size_t root_certs_size = sizeof(root_certs) / sizeof(root_certs[0]);
static X509* root_cert_instances[root_certs_size] = {NULL};
static atomic_flag root_cert_instances_lock = ATOMIC_FLAG_INIT;
static atomic_bool root_cert_instances_initialized = 0;

// Native root certs
static size_t native_certs_size = 0;
static X509 **native_cert_instances = NULL;
static atomic_flag native_cert_instances_lock = ATOMIC_FLAG_INIT;
static atomic_bool native_cert_instances_initialized = 0;

struct loop_ssl_data {
char *ssl_read_input, *ssl_read_output;
unsigned int ssl_read_input_length;
Expand Down Expand Up @@ -586,6 +690,204 @@ int us_no_password_callback(char* buf, int size, int rwflag, void* u) {
return 0;
}

#if defined(__APPLE__) && defined(__MACH__)

// Loads CF & Security frameworks and their functions into cf_funcs and sec_funcs.
int us_internal_osx_load_frameworks(void) {
// Load CoreFoundation and Security Framework
void *cf_hnd = dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", RTLD_LAZY);
void *sec_hnd = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY);
if (!cf_hnd || !sec_hnd) {
return 0;
}

// Helper macros for loading functions. None of these symbols should ever be NULL.
#define LOAD_CF_FN(name, type) if ((cf_funcs.name = (type) dlsym(cf_hnd, #name)) == NULL) return 0
#define LOAD_SEC_FN(name, type) if ((sec_funcs.name = (type) dlsym(sec_hnd, #name)) == NULL) return 0

// Load functions from CoreFoundation
cf_funcs.handle = cf_hnd;
LOAD_CF_FN(CFRelease, void (*)(void *));
LOAD_CF_FN(CFArrayCreateMutable, CFMutableArrayRef(*)(void *, CFIndex, void *));
LOAD_CF_FN(CFArrayGetValueAtIndex, const void *(*)(CFArrayRef, CFIndex));
LOAD_CF_FN(CFArrayGetCount, CFIndex(*)(CFArrayRef));
LOAD_CF_FN(CFArrayAppendValue, void(*)(CFMutableArrayRef, const void *));
LOAD_CF_FN(CFDataGetBytePtr, const uint8_t *(*)(CFDataRef));
LOAD_CF_FN(CFDataGetLength, CFIndex(*)(CFDataRef));
LOAD_CF_FN(__CFStringMakeConstantString, CFStringRef(*)(const char *));
LOAD_CF_FN(CFStringCompare, CFComparisonResult(*)(CFStringRef, CFStringRef, unsigned long));
LOAD_CF_FN(CFDictionaryGetValueIfPresent, Boolean(*)(CFDictionaryRef, const void *, const void **));
LOAD_CF_FN(CFDictionaryGetValue, const void *(*)(CFDictionaryRef, const void *));
LOAD_CF_FN(CFNumberGetValue, Boolean(*)(CFNumberRef, CFNumberType, void *));

// Load functions from Security Framework
sec_funcs.handle = sec_hnd;
LOAD_SEC_FN(SecCertificateCopyData, CFDataRef(*)(SecCertificateRef));
LOAD_SEC_FN(SecTrustSettingsCopyCertificates, OSStatus(*)(SecTrustSettingsDomain, CFArrayRef * ));
LOAD_SEC_FN(SecTrustSettingsCopyTrustSettings,
OSStatus(*)(SecCertificateRef, SecTrustSettingsDomain, CFArrayRef * ));

#undef LOAD_SEC_FN
#undef LOAD_CF_FN

return 1;
}

// Places aggregate trust setting for a given certificate from a domain into result.
int us_internal_osx_cert_trust(SecTrustSettingsDomain domain, SecCertificateRef cert, SecTrustSettingsResult *result) {
// Trust settings: CFArray of CFDictionary objects
CFArrayRef trust_settings = NULL;

if (sec_funcs.SecTrustSettingsCopyTrustSettings(cert, domain, &trust_settings) == errSecItemNotFound)
return 0;

// From Apple Docs:
// The trustSettings parameter can return a valid but empty CFArrayRef. This
// empty trust-settings array means “always trust this certificate” with an
// overall trust setting for the certificate of
// kSecTrustSettingsResultTrustRoot. However, an empty trust settings array
// isn’t the same as no trust settings, where the trustSettings parameter
// returns NULL. No trust-settings array means “this certificate must be
// verifiable using a known trusted certificate”.
if (trust_settings == NULL)
return 0;

for (CFIndex i = 0; i < cf_funcs.CFArrayGetCount(trust_settings); i++) {
CFStringRef policy_name;
int32_t trust_result;

CFDictionaryRef setting = (CFDictionaryRef) cf_funcs.CFArrayGetValueAtIndex(trust_settings, i);

// Reject settings for non-ssl policies
if (!cf_funcs.CFDictionaryGetValueIfPresent(setting, CFSTR("kSecTrustSettingsPolicyName"),
(const void **) &policy_name)) {
continue;
}
if (cf_funcs.CFStringCompare(policy_name, CFSTR("sslServer"), 0) != kCFCompareEqualTo)
continue;

// Get the trust result
CFNumberRef cf_trust_result = (CFNumberRef) cf_funcs.CFDictionaryGetValue(setting, kSecTrustSettingsResult);
if (cf_trust_result == NULL)
// See large comment above
trust_result = kSecTrustSettingsResultTrustRoot;
else if (!cf_funcs.CFNumberGetValue(cf_trust_result, kCFNumberSInt32Type, &trust_result))
trust_result = kSecTrustSettingsResultInvalid;

switch (trust_result) {
case kSecTrustSettingsResultInvalid:
case kSecTrustSettingsResultUnspecified:
continue;
default:
*result = (SecTrustSettingsResult) trust_result;
goto end;
}
}

// See large comment above
*result = kSecTrustSettingsResultTrustRoot;
end:
cf_funcs.CFRelease(trust_settings);
return 1;
}

// Appends the trusted/root certificates in a given domain to certs.
int us_internal_osx_trusted_certs(SecTrustSettingsDomain domain, CFMutableArrayRef certs) {
int ret = 1;
// CFArray of SecCertificateRef
CFArrayRef tmp;

OSStatus result = sec_funcs.SecTrustSettingsCopyCertificates(domain, &tmp);
switch (result) {
case errSecSuccess:
break;
case errSecNoTrustSettings:
// No trust settings means no trusted certificates.
// Do nothing & return success.
return 1;
default:
return 0;
}

for (CFIndex i = 0; i < cf_funcs.CFArrayGetCount(tmp); i++) {
SecTrustSettingsResult trust = kSecTrustSettingsResultUnspecified;

SecCertificateRef cert = (SecCertificateRef) cf_funcs.CFArrayGetValueAtIndex(tmp, i);
if (!us_internal_osx_cert_trust(domain, cert, &trust)) {
ret = 0;
goto end;
}

if (trust == kSecTrustSettingsResultTrustRoot || trust == kSecTrustSettingsResultTrustAsRoot)
cf_funcs.CFArrayAppendValue(certs, cert);
}

end:
cf_funcs.CFRelease(tmp);
return ret;
}

// Initializes native_certs with the trusted certificates in the OSX keychain.
// Must be called with native_cert_instances_lock held.
void us_internal_osx_init_native_certs(void) {
if (!us_internal_osx_load_frameworks()) {
OPENSSL_PUT_ERROR(SSL, ERR_R_SYS_LIB);
return;
}

// CFMutableArray of SecCertificateRef
CFMutableArrayRef certs = cf_funcs.CFArrayCreateMutable(NULL, 0, dlsym(cf_funcs.handle, "kCFTypeArrayCallBacks"));

if (!us_internal_osx_trusted_certs(kSecTrustSettingsDomainUser, certs) ||
!us_internal_osx_trusted_certs(kSecTrustSettingsDomainAdmin, certs) ||
!us_internal_osx_trusted_certs(kSecTrustSettingsDomainSystem, certs)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_SYS_LIB);
return;
}

native_certs_size = (size_t) cf_funcs.CFArrayGetCount(certs);
native_cert_instances = us_malloc(sizeof(X509 *) * native_certs_size);

for (CFIndex i = 0; i < (CFIndex) native_certs_size; ++i) {
SecCertificateRef sec_cert = (SecCertificateRef) cf_funcs.CFArrayGetValueAtIndex(certs, i);

CFDataRef der = sec_funcs.SecCertificateCopyData(sec_cert);
const unsigned char *buf = cf_funcs.CFDataGetBytePtr(der);

X509 *ssl_cert = d2i_X509(NULL, &buf, cf_funcs.CFDataGetLength(der));

if (ssl_cert == NULL)
OPENSSL_PUT_ERROR(SSL, ERR_R_X509_LIB);

native_cert_instances[i] = ssl_cert;

cf_funcs.CFRelease(der);
}

cf_funcs.CFRelease(certs);

// Close the frameworks, we don't need them anymore
dlclose(sec_funcs.handle);
dlclose(cf_funcs.handle);
}

#endif

void us_internal_init_native_certs(void) {
if (atomic_load(&native_cert_instances_initialized) == 1) return;

while (atomic_flag_test_and_set_explicit(&native_cert_instances_lock, memory_order_acquire));

if (atomic_load(&native_cert_instances_initialized) == 0) {
#if defined(__APPLE__) && defined(__MACH__)
us_internal_osx_init_native_certs();
#endif

atomic_store(&native_cert_instances_initialized, 1);
}

atomic_flag_clear_explicit(&native_cert_instances_lock, memory_order_release);
}

static X509 * us_ssl_ctx_get_X509_without_callback_from(struct us_cert_string_t content) {
X509 *x = NULL;
Expand Down Expand Up @@ -647,6 +949,7 @@ X509_STORE* us_get_default_ca_store() {
}

us_internal_init_root_certs();
us_internal_init_native_certs();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do this, we should just use the native certs and not both right? So on macOS, it would only load native certs and on Linux, it would only load root certs.

I think that we also should see if there's any perf difference in load time as I remember a 30ms perf hit a long time ago when loading native certs using a non-OpenSSL/BoringSSL library and loading from the system trust store.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did some profiling, and the results are not good. The 3 SecTrustSettingsCopyCertificates requests to get all the certificates took 70ms total & the SecTrustSettingsCopyTrustSettings on each certificate to determine whether the system trusts the cert took 78ms total. As of 1.0.6, bun only takes 6ms to load certificates.

That's really disappointing. I was not expecting these APIs to be so slow.

I think a better approach would be to follow what Deno did in denoland/deno#11491. On all platforms, default to embedded Mozilla certs, and have some flag/env variable/config to enable the use of native certs for those who want/need it and are willing to take the first request performance hit.


// load all root_cert_instances on the default ca store
for (size_t i = 0; i < root_certs_size; i++) {
Expand All @@ -655,7 +958,15 @@ X509_STORE* us_get_default_ca_store() {
X509_up_ref(cert);
X509_STORE_add_cert(store, cert);
}


// load all native_certs on the default ca store
for (size_t i = 0; i < native_certs_size; i++) {
X509 *cert = native_cert_instances[i];
if (cert == NULL) continue;
X509_up_ref(cert);
X509_STORE_add_cert(store, cert);
}

return store;
}

Expand Down
Loading