mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
crypto: added support for reading certificates from macOS system store
PR-URL: https://github.com/nodejs/node/pull/56599 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
parent
64ee8a0258
commit
efe698ee93
11 changed files with 388 additions and 9 deletions
|
@ -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
|
||||
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"',
|
||||
],
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -341,6 +341,7 @@ class PerProcessOptions : public Options {
|
|||
bool ssl_openssl_cert_store = false;
|
||||
#endif
|
||||
bool use_openssl_ca = false;
|
||||
bool use_system_ca = false;
|
||||
bool use_bundled_ca = false;
|
||||
bool enable_fips_crypto = false;
|
||||
bool force_fips_crypto = false;
|
||||
|
|
|
@ -19,6 +19,9 @@ test-fs-read-stream-concurrent-reads: PASS, FLAKY
|
|||
# https://github.com/nodejs/build/issues/3043
|
||||
test-snapshot-incompatible: SKIP
|
||||
|
||||
# Requires manual setup for certificates to be trusted by the system
|
||||
test-native-certs-macos: SKIP
|
||||
|
||||
[$system==win32]
|
||||
# https://github.com/nodejs/node/issues/54808
|
||||
test-async-context-frame: PASS, FLAKY
|
||||
|
|
|
@ -67,6 +67,9 @@ if (common.hasCrypto) {
|
|||
expectNoWorker('--use-bundled-ca', 'B\n');
|
||||
if (!hasOpenSSL3)
|
||||
expectNoWorker('--openssl-config=_ossl_cfg', 'B\n');
|
||||
if (common.isMacOS) {
|
||||
expectNoWorker('--use-system-ca', 'B\n');
|
||||
}
|
||||
}
|
||||
|
||||
// V8 options
|
||||
|
|
|
@ -26,7 +26,7 @@ function validateNodePrintHelp() {
|
|||
const cliHelpOptions = [
|
||||
{ compileConstant: HAVE_OPENSSL,
|
||||
flags: [ '--openssl-config=...', '--tls-cipher-list=...',
|
||||
'--use-bundled-ca', '--use-openssl-ca',
|
||||
'--use-bundled-ca', '--use-openssl-ca', '--use-system-ca',
|
||||
'--enable-fips', '--force-fips' ] },
|
||||
{ compileConstant: NODE_HAVE_I18N_SUPPORT,
|
||||
flags: [ '--icu-data-dir=...', 'NODE_ICU_DATA' ] },
|
||||
|
|
47
test/parallel/test-native-certs-macos.mjs
Normal file
47
test/parallel/test-native-certs-macos.mjs
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Flags: --use-system-ca
|
||||
|
||||
import * as common from '../common/index.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import https from 'node:https';
|
||||
import fixtures from '../common/fixtures.js';
|
||||
import { it, beforeEach, afterEach, describe } from 'node:test';
|
||||
import { once } from 'events';
|
||||
|
||||
const handleRequest = (req, res) => {
|
||||
const path = req.url;
|
||||
switch (path) {
|
||||
case '/hello-world':
|
||||
res.writeHead(200);
|
||||
res.end('hello world\n');
|
||||
break;
|
||||
default:
|
||||
assert(false, `Unexpected path: ${path}`);
|
||||
}
|
||||
};
|
||||
|
||||
describe('use-system-ca', { skip: !common.isMacOS }, function() {
|
||||
let server;
|
||||
|
||||
beforeEach(async function() {
|
||||
server = https.createServer({
|
||||
key: fixtures.readKey('agent8-key.pem'),
|
||||
cert: fixtures.readKey('agent8-cert.pem'),
|
||||
}, handleRequest);
|
||||
server.listen(0);
|
||||
await once(server, 'listening');
|
||||
});
|
||||
|
||||
it('can connect successfully with a trusted certificate', async function() {
|
||||
// Requires trusting the CA certificate first (which needs an interactive GUI approval, e.g. TouchID):
|
||||
// security add-trusted-cert \
|
||||
// -k /Users/$USER/Library/Keychains/login.keychain-db test/fixtures/keys/fake-startcom-root-cert.pem
|
||||
// To remove:
|
||||
// security delete-certificate -c 'StartCom Certification Authority' \
|
||||
// -t /Users/$USER/Library/Keychains/login.keychain-db
|
||||
await fetch(`https://localhost:${server.address().port}/hello-world`);
|
||||
});
|
||||
|
||||
afterEach(async function() {
|
||||
server?.close();
|
||||
});
|
||||
});
|
|
@ -61,6 +61,7 @@ const conditionalOpts = [
|
|||
'--tls-cipher-list',
|
||||
'--use-bundled-ca',
|
||||
'--use-openssl-ca',
|
||||
common.isMacOS ? '--use-system-ca' : '',
|
||||
'--secure-heap',
|
||||
'--secure-heap-min',
|
||||
'--enable-fips',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue