Skip to content

Commit

Permalink
crypto: added support for reading certificates from macOS system store
Browse files Browse the repository at this point in the history
PR-URL: #56599
Reviewed-By: James M Snell <[email protected]>
Reviewed-By: Joyee Cheung <[email protected]>
  • Loading branch information
timja authored Jan 28, 2025
1 parent 64ee8a0 commit efe698e
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 9 deletions.
8 changes: 8 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -2861,6 +2861,13 @@ The following values are valid for `mode`:
* `silent`: If supported by the OS, mapping will be attempted. Failure to map
will be ignored and will not be reported.

### `--use-system-ca`

Node.js uses the trusted CA certificates present in the system store along with
the `--use-bundled-ca`, `--use-openssl-ca` options.

This option is available to macOS only.

### `--v8-options`

<!-- YAML
Expand Down Expand Up @@ -3260,6 +3267,7 @@ one is included in the list below.
* `--use-bundled-ca`
* `--use-largepages`
* `--use-openssl-ca`
* `--use-system-ca`
* `--v8-pool-size`
* `--watch-path`
* `--watch-preserve-output`
Expand Down
3 changes: 3 additions & 0 deletions doc/api/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,9 @@ from the bundled Mozilla CA store as supplied by the current Node.js version.
The bundled CA store, as supplied by Node.js, is a snapshot of Mozilla CA store
that is fixed at release time. It is identical on all supported platforms.

On macOS if `--use-system-ca` is passed then trusted certificates
from the user and system keychains are also included.

## `tls.DEFAULT_ECDH_CURVE`

<!-- YAML
Expand Down
5 changes: 3 additions & 2 deletions node.gypi
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,9 @@

[ 'OS=="mac"', {
# linking Corefoundation is needed since certain macOS debugging tools
# like Instruments require it for some features
'libraries': [ '-framework CoreFoundation' ],
# like Instruments require it for some features. Security is needed for
# --use-system-ca.
'libraries': [ '-framework CoreFoundation -framework Security' ],
'defines!': [
'NODE_PLATFORM="mac"',
],
Expand Down
320 changes: 314 additions & 6 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
#ifndef OPENSSL_NO_ENGINE
#include <openssl/engine.h>
#endif // !OPENSSL_NO_ENGINE
#ifdef __APPLE__
#include <Security/Security.h>
#endif

namespace node {

Expand Down Expand Up @@ -232,6 +235,306 @@ unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
}
}

// Indicates the trust status of a certificate.
enum class TrustStatus {
// Trust status is unknown / uninitialized.
UNKNOWN,
// Certificate inherits trust value from its issuer. If the certificate is the
// root of the chain, this implies distrust.
UNSPECIFIED,
// Certificate is a trust anchor.
TRUSTED,
// Certificate is blocked / explicitly distrusted.
DISTRUSTED
};

bool isSelfIssued(X509* cert) {
auto subject = X509_get_subject_name(cert);
auto issuer = X509_get_issuer_name(cert);

return X509_NAME_cmp(subject, issuer) == 0;
}

#ifdef __APPLE__
// This code is loosely based on
// https://github.com/chromium/chromium/blob/54bd8e3/net/cert/internal/trust_store_mac.cc
// Copyright 2015 The Chromium Authors
// Licensed under a BSD-style license
// See https://chromium.googlesource.com/chromium/src/+/HEAD/LICENSE for
// details.
TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict,
bool is_self_issued) {
// Trust settings may be scoped to a single application
// skip as this is not supported
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) {
return TrustStatus::UNSPECIFIED;
}

// Trust settings may be scoped using policy-specific constraints. For
// example, SSL trust settings might be scoped to a single hostname, or EAP
// settings specific to a particular WiFi network.
// As this is not presently supported, skip any policy-specific trust
// settings.
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsPolicyString)) {
return TrustStatus::UNSPECIFIED;
}

// If the trust settings are scoped to a specific policy (via
// kSecTrustSettingsPolicy), ensure that the policy is the same policy as
// |kSecPolicyAppleSSL|. If there is no kSecTrustSettingsPolicy key, it's
// considered a match for all policies.
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsPolicy)) {
SecPolicyRef policy_ref = reinterpret_cast<SecPolicyRef>(const_cast<void*>(
CFDictionaryGetValue(trust_dict, kSecTrustSettingsPolicy)));

if (!policy_ref) {
return TrustStatus::UNSPECIFIED;
}

CFDictionaryRef policy_dict(SecPolicyCopyProperties(policy_ref));

// kSecPolicyOid is guaranteed to be present in the policy dictionary.
CFStringRef policy_oid = reinterpret_cast<CFStringRef>(
const_cast<void*>(CFDictionaryGetValue(policy_dict, kSecPolicyOid)));

if (!CFEqual(policy_oid, kSecPolicyAppleSSL)) {
return TrustStatus::UNSPECIFIED;
}
}

int trust_settings_result = kSecTrustSettingsResultTrustRoot;
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsResult)) {
CFNumberRef trust_settings_result_ref =
reinterpret_cast<CFNumberRef>(const_cast<void*>(
CFDictionaryGetValue(trust_dict, kSecTrustSettingsResult)));

if (!trust_settings_result_ref ||
!CFNumberGetValue(trust_settings_result_ref,
kCFNumberIntType,
&trust_settings_result)) {
return TrustStatus::UNSPECIFIED;
}

if (trust_settings_result == kSecTrustSettingsResultDeny) {
return TrustStatus::DISTRUSTED;
}

// This is a bit of a hack: if the cert is self-issued allow either
// kSecTrustSettingsResultTrustRoot or kSecTrustSettingsResultTrustAsRoot on
// the basis that SecTrustSetTrustSettings should not allow creating an
// invalid trust record in the first place. (The spec is that
// kSecTrustSettingsResultTrustRoot can only be applied to root(self-signed)
// certs and kSecTrustSettingsResultTrustAsRoot is used for other certs.)
// This hack avoids having to check the signature on the cert which is slow
// if using the platform APIs, and may require supporting MD5 signature
// algorithms on some older OSX versions or locally added roots, which is
// undesirable in the built-in signature verifier.
if (is_self_issued) {
return trust_settings_result == kSecTrustSettingsResultTrustRoot ||
trust_settings_result == kSecTrustSettingsResultTrustAsRoot
? TrustStatus::TRUSTED
: TrustStatus::UNSPECIFIED;
}

// kSecTrustSettingsResultTrustAsRoot can only be applied to non-root certs.
return (trust_settings_result == kSecTrustSettingsResultTrustAsRoot)
? TrustStatus::TRUSTED
: TrustStatus::UNSPECIFIED;
}

return TrustStatus::UNSPECIFIED;
}

TrustStatus IsTrustSettingsTrustedForPolicy(CFArrayRef trust_settings,
bool is_self_issued) {
// The trust_settings 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
if (CFArrayGetCount(trust_settings) == 0) {
return is_self_issued ? TrustStatus::TRUSTED : TrustStatus::UNSPECIFIED;
}

for (CFIndex i = 0; i < CFArrayGetCount(trust_settings); ++i) {
CFDictionaryRef trust_dict = reinterpret_cast<CFDictionaryRef>(
const_cast<void*>(CFArrayGetValueAtIndex(trust_settings, i)));

TrustStatus trust =
IsTrustDictionaryTrustedForPolicy(trust_dict, is_self_issued);

if (trust == TrustStatus::DISTRUSTED || trust == TrustStatus::TRUSTED) {
return trust;
}
}
return TrustStatus::UNSPECIFIED;
}

bool IsCertificateTrustValid(SecCertificateRef ref) {
SecTrustRef sec_trust = nullptr;
CFMutableArrayRef subj_certs =
CFArrayCreateMutable(nullptr, 1, &kCFTypeArrayCallBacks);
CFArraySetValueAtIndex(subj_certs, 0, ref);

SecPolicyRef policy = SecPolicyCreateSSL(false, nullptr);
OSStatus ortn =
SecTrustCreateWithCertificates(subj_certs, policy, &sec_trust);
bool result = false;
if (ortn) {
/* should never happen */
} else {
result = SecTrustEvaluateWithError(sec_trust, nullptr);
}

if (policy) {
CFRelease(policy);
}
if (sec_trust) {
CFRelease(sec_trust);
}
if (subj_certs) {
CFRelease(subj_certs);
}
return result;
}

bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) {
OSStatus err;

bool trust_evaluated = false;
bool is_self_issued = isSelfIssued(cert);

// Evaluate user trust domain, then admin. User settings can override
// admin (and both override the system domain, but we don't check that).
for (const auto& trust_domain :
{kSecTrustSettingsDomainUser, kSecTrustSettingsDomainAdmin}) {
CFArrayRef trust_settings = nullptr;
err = SecTrustSettingsCopyTrustSettings(ref, trust_domain, &trust_settings);

if (err != errSecSuccess && err != errSecItemNotFound) {
fprintf(stderr,
"ERROR: failed to copy trust settings of system certificate%d\n",
err);
continue;
}

if (err == errSecSuccess && trust_settings != nullptr) {
TrustStatus result =
IsTrustSettingsTrustedForPolicy(trust_settings, is_self_issued);
if (result != TrustStatus::UNSPECIFIED) {
CFRelease(trust_settings);
return result == TrustStatus::TRUSTED;
}
}

// An empty trust settings array isn’t the same as no trust settings,
// where the trust_settings parameter returns NULL.
// No trust-settings array means
// “this certificate must be verifiable using a known trusted certificate”.
if (trust_settings == nullptr && !trust_evaluated) {
bool result = IsCertificateTrustValid(ref);
if (result) {
return true;
}
// no point re-evaluating this in the admin domain
trust_evaluated = true;
} else if (trust_settings) {
CFRelease(trust_settings);
}
}
return false;
}

void ReadMacOSKeychainCertificates(
std::vector<std::string>* system_root_certificates) {
CFTypeRef search_keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
CFTypeRef search_values[] = {
kSecClassCertificate, kSecMatchLimitAll, kCFBooleanTrue};
CFDictionaryRef search = CFDictionaryCreate(kCFAllocatorDefault,
search_keys,
search_values,
3,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);

CFArrayRef curr_anchors = nullptr;
OSStatus ortn =
SecItemCopyMatching(search, reinterpret_cast<CFTypeRef*>(&curr_anchors));
CFRelease(search);

if (ortn) {
fprintf(stderr, "ERROR: SecItemCopyMatching failed %d\n", ortn);
}

CFIndex count = CFArrayGetCount(curr_anchors);

std::vector<X509*> system_root_certificates_X509;
for (int i = 0; i < count; ++i) {
SecCertificateRef cert_ref = reinterpret_cast<SecCertificateRef>(
const_cast<void*>(CFArrayGetValueAtIndex(curr_anchors, i)));

CFDataRef der_data = SecCertificateCopyData(cert_ref);
if (!der_data) {
fprintf(stderr, "ERROR: SecCertificateCopyData failed\n");
continue;
}
auto data_buffer_pointer = CFDataGetBytePtr(der_data);

X509* cert =
d2i_X509(nullptr, &data_buffer_pointer, CFDataGetLength(der_data));
CFRelease(der_data);
bool is_valid = IsCertificateTrustedForPolicy(cert, cert_ref);
if (is_valid) {
system_root_certificates_X509.emplace_back(cert);
}
}
CFRelease(curr_anchors);

for (size_t i = 0; i < system_root_certificates_X509.size(); i++) {
ncrypto::X509View x509_view(system_root_certificates_X509[i]);

auto pem_bio = x509_view.toPEM();
if (!pem_bio) {
fprintf(stderr,
"Warning: converting system certificate to PEM format failed\n");
continue;
}

char* pem_data = nullptr;
auto pem_size = BIO_get_mem_data(pem_bio.get(), &pem_data);
if (pem_size <= 0 || !pem_data) {
fprintf(
stderr,
"Warning: cannot read PEM-encoded data from system certificate\n");
continue;
}
std::string certificate_string_pem(pem_data, pem_size);

system_root_certificates->emplace_back(certificate_string_pem);
}
}
#endif // __APPLE__

void ReadSystemStoreCertificates(
std::vector<std::string>* system_root_certificates) {
#ifdef __APPLE__
ReadMacOSKeychainCertificates(system_root_certificates);
#endif
}

std::vector<std::string> getCombinedRootCertificates() {
std::vector<std::string> combined_root_certs;

for (size_t i = 0; i < arraysize(root_certs); i++) {
combined_root_certs.emplace_back(root_certs[i]);
}

if (per_process::cli_options->use_system_ca) {
ReadSystemStoreCertificates(&combined_root_certs);
}

return combined_root_certs;
}

X509_STORE* NewRootCertStore() {
static std::vector<X509*> root_certs_vector;
static bool root_certs_vector_loaded = false;
Expand All @@ -240,12 +543,17 @@ X509_STORE* NewRootCertStore() {

if (!root_certs_vector_loaded) {
if (per_process::cli_options->ssl_openssl_cert_store == false) {
for (size_t i = 0; i < arraysize(root_certs); i++) {
X509* x509 = PEM_read_bio_X509(
NodeBIO::NewFixed(root_certs[i], strlen(root_certs[i])).get(),
nullptr, // no re-use of X509 structure
NoPasswordCallback,
nullptr); // no callback data
std::vector<std::string> combined_root_certs =
getCombinedRootCertificates();

for (size_t i = 0; i < combined_root_certs.size(); i++) {
X509* x509 =
PEM_read_bio_X509(NodeBIO::NewFixed(combined_root_certs[i].data(),
combined_root_certs[i].length())
.get(),
nullptr, // no re-use of X509 structure
NoPasswordCallback,
nullptr); // no callback data

// Parse errors from the built-in roots are fatal.
CHECK_NOT_NULL(x509);
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
,
&PerProcessOptions::use_openssl_ca,
kAllowedInEnvvar);
AddOption("--use-system-ca",
"use system's CA store",
&PerProcessOptions::use_system_ca,
kAllowedInEnvvar);
AddOption("--use-bundled-ca",
"use bundled CA store"
#if !defined(NODE_OPENSSL_CERT_STORE)
Expand Down
Loading

0 comments on commit efe698e

Please sign in to comment.