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

feat: added support for reading certificates from macOS system store #56599

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
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
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
timja marked this conversation as resolved.
Show resolved Hide resolved
// 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;
}

timja marked this conversation as resolved.
Show resolved Hide resolved
// 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);

timja marked this conversation as resolved.
Show resolved Hide resolved
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);
timja marked this conversation as resolved.
Show resolved Hide resolved
}
}
#endif // __APPLE__

timja marked this conversation as resolved.
Show resolved Hide resolved
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
Loading