Skip to content

Commit 39a474f

Browse files
timjatargos
authored andcommitted
crypto: added support for reading certificates from macOS system store
PR-URL: #56599 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Joyee Cheung <[email protected]>
1 parent 4e3052a commit 39a474f

11 files changed

+388
-9
lines changed

doc/api/cli.md

+8
Original file line numberDiff line numberDiff line change
@@ -2833,6 +2833,13 @@ The following values are valid for `mode`:
28332833
* `silent`: If supported by the OS, mapping will be attempted. Failure to map
28342834
will be ignored and will not be reported.
28352835

2836+
### `--use-system-ca`
2837+
2838+
Node.js uses the trusted CA certificates present in the system store along with
2839+
the `--use-bundled-ca`, `--use-openssl-ca` options.
2840+
2841+
This option is available to macOS only.
2842+
28362843
### `--v8-options`
28372844

28382845
<!-- YAML
@@ -3233,6 +3240,7 @@ one is included in the list below.
32333240
* `--use-bundled-ca`
32343241
* `--use-largepages`
32353242
* `--use-openssl-ca`
3243+
* `--use-system-ca`
32363244
* `--v8-pool-size`
32373245
* `--watch-path`
32383246
* `--watch-preserve-output`

doc/api/tls.md

+3
Original file line numberDiff line numberDiff line change
@@ -2400,6 +2400,9 @@ from the bundled Mozilla CA store as supplied by the current Node.js version.
24002400
The bundled CA store, as supplied by Node.js, is a snapshot of Mozilla CA store
24012401
that is fixed at release time. It is identical on all supported platforms.
24022402

2403+
On macOS if `--use-system-ca` is passed then trusted certificates
2404+
from the user and system keychains are also included.
2405+
24032406
## `tls.DEFAULT_ECDH_CURVE`
24042407

24052408
<!-- YAML

node.gypi

+3-2
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,9 @@
238238

239239
[ 'OS=="mac"', {
240240
# linking Corefoundation is needed since certain macOS debugging tools
241-
# like Instruments require it for some features
242-
'libraries': [ '-framework CoreFoundation' ],
241+
# like Instruments require it for some features. Security is needed for
242+
# --use-system-ca.
243+
'libraries': [ '-framework CoreFoundation -framework Security' ],
243244
'defines!': [
244245
'NODE_PLATFORM="mac"',
245246
],

src/crypto/crypto_context.cc

+314-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
#ifndef OPENSSL_NO_ENGINE
1919
#include <openssl/engine.h>
2020
#endif // !OPENSSL_NO_ENGINE
21+
#ifdef __APPLE__
22+
#include <Security/Security.h>
23+
#endif
2124

2225
namespace node {
2326

@@ -232,6 +235,306 @@ unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
232235
}
233236
}
234237

238+
// Indicates the trust status of a certificate.
239+
enum class TrustStatus {
240+
// Trust status is unknown / uninitialized.
241+
UNKNOWN,
242+
// Certificate inherits trust value from its issuer. If the certificate is the
243+
// root of the chain, this implies distrust.
244+
UNSPECIFIED,
245+
// Certificate is a trust anchor.
246+
TRUSTED,
247+
// Certificate is blocked / explicitly distrusted.
248+
DISTRUSTED
249+
};
250+
251+
bool isSelfIssued(X509* cert) {
252+
auto subject = X509_get_subject_name(cert);
253+
auto issuer = X509_get_issuer_name(cert);
254+
255+
return X509_NAME_cmp(subject, issuer) == 0;
256+
}
257+
258+
#ifdef __APPLE__
259+
// This code is loosely based on
260+
// https://github.com/chromium/chromium/blob/54bd8e3/net/cert/internal/trust_store_mac.cc
261+
// Copyright 2015 The Chromium Authors
262+
// Licensed under a BSD-style license
263+
// See https://chromium.googlesource.com/chromium/src/+/HEAD/LICENSE for
264+
// details.
265+
TrustStatus IsTrustDictionaryTrustedForPolicy(CFDictionaryRef trust_dict,
266+
bool is_self_issued) {
267+
// Trust settings may be scoped to a single application
268+
// skip as this is not supported
269+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) {
270+
return TrustStatus::UNSPECIFIED;
271+
}
272+
273+
// Trust settings may be scoped using policy-specific constraints. For
274+
// example, SSL trust settings might be scoped to a single hostname, or EAP
275+
// settings specific to a particular WiFi network.
276+
// As this is not presently supported, skip any policy-specific trust
277+
// settings.
278+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsPolicyString)) {
279+
return TrustStatus::UNSPECIFIED;
280+
}
281+
282+
// If the trust settings are scoped to a specific policy (via
283+
// kSecTrustSettingsPolicy), ensure that the policy is the same policy as
284+
// |kSecPolicyAppleSSL|. If there is no kSecTrustSettingsPolicy key, it's
285+
// considered a match for all policies.
286+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsPolicy)) {
287+
SecPolicyRef policy_ref = reinterpret_cast<SecPolicyRef>(const_cast<void*>(
288+
CFDictionaryGetValue(trust_dict, kSecTrustSettingsPolicy)));
289+
290+
if (!policy_ref) {
291+
return TrustStatus::UNSPECIFIED;
292+
}
293+
294+
CFDictionaryRef policy_dict(SecPolicyCopyProperties(policy_ref));
295+
296+
// kSecPolicyOid is guaranteed to be present in the policy dictionary.
297+
CFStringRef policy_oid = reinterpret_cast<CFStringRef>(
298+
const_cast<void*>(CFDictionaryGetValue(policy_dict, kSecPolicyOid)));
299+
300+
if (!CFEqual(policy_oid, kSecPolicyAppleSSL)) {
301+
return TrustStatus::UNSPECIFIED;
302+
}
303+
}
304+
305+
int trust_settings_result = kSecTrustSettingsResultTrustRoot;
306+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsResult)) {
307+
CFNumberRef trust_settings_result_ref =
308+
reinterpret_cast<CFNumberRef>(const_cast<void*>(
309+
CFDictionaryGetValue(trust_dict, kSecTrustSettingsResult)));
310+
311+
if (!trust_settings_result_ref ||
312+
!CFNumberGetValue(trust_settings_result_ref,
313+
kCFNumberIntType,
314+
&trust_settings_result)) {
315+
return TrustStatus::UNSPECIFIED;
316+
}
317+
318+
if (trust_settings_result == kSecTrustSettingsResultDeny) {
319+
return TrustStatus::DISTRUSTED;
320+
}
321+
322+
// This is a bit of a hack: if the cert is self-issued allow either
323+
// kSecTrustSettingsResultTrustRoot or kSecTrustSettingsResultTrustAsRoot on
324+
// the basis that SecTrustSetTrustSettings should not allow creating an
325+
// invalid trust record in the first place. (The spec is that
326+
// kSecTrustSettingsResultTrustRoot can only be applied to root(self-signed)
327+
// certs and kSecTrustSettingsResultTrustAsRoot is used for other certs.)
328+
// This hack avoids having to check the signature on the cert which is slow
329+
// if using the platform APIs, and may require supporting MD5 signature
330+
// algorithms on some older OSX versions or locally added roots, which is
331+
// undesirable in the built-in signature verifier.
332+
if (is_self_issued) {
333+
return trust_settings_result == kSecTrustSettingsResultTrustRoot ||
334+
trust_settings_result == kSecTrustSettingsResultTrustAsRoot
335+
? TrustStatus::TRUSTED
336+
: TrustStatus::UNSPECIFIED;
337+
}
338+
339+
// kSecTrustSettingsResultTrustAsRoot can only be applied to non-root certs.
340+
return (trust_settings_result == kSecTrustSettingsResultTrustAsRoot)
341+
? TrustStatus::TRUSTED
342+
: TrustStatus::UNSPECIFIED;
343+
}
344+
345+
return TrustStatus::UNSPECIFIED;
346+
}
347+
348+
TrustStatus IsTrustSettingsTrustedForPolicy(CFArrayRef trust_settings,
349+
bool is_self_issued) {
350+
// The trust_settings parameter can return a valid but empty CFArrayRef.
351+
// This empty trust-settings array means “always trust this certificate”
352+
// with an overall trust setting for the certificate of
353+
// kSecTrustSettingsResultTrustRoot
354+
if (CFArrayGetCount(trust_settings) == 0) {
355+
return is_self_issued ? TrustStatus::TRUSTED : TrustStatus::UNSPECIFIED;
356+
}
357+
358+
for (CFIndex i = 0; i < CFArrayGetCount(trust_settings); ++i) {
359+
CFDictionaryRef trust_dict = reinterpret_cast<CFDictionaryRef>(
360+
const_cast<void*>(CFArrayGetValueAtIndex(trust_settings, i)));
361+
362+
TrustStatus trust =
363+
IsTrustDictionaryTrustedForPolicy(trust_dict, is_self_issued);
364+
365+
if (trust == TrustStatus::DISTRUSTED || trust == TrustStatus::TRUSTED) {
366+
return trust;
367+
}
368+
}
369+
return TrustStatus::UNSPECIFIED;
370+
}
371+
372+
bool IsCertificateTrustValid(SecCertificateRef ref) {
373+
SecTrustRef sec_trust = nullptr;
374+
CFMutableArrayRef subj_certs =
375+
CFArrayCreateMutable(nullptr, 1, &kCFTypeArrayCallBacks);
376+
CFArraySetValueAtIndex(subj_certs, 0, ref);
377+
378+
SecPolicyRef policy = SecPolicyCreateSSL(false, nullptr);
379+
OSStatus ortn =
380+
SecTrustCreateWithCertificates(subj_certs, policy, &sec_trust);
381+
bool result = false;
382+
if (ortn) {
383+
/* should never happen */
384+
} else {
385+
result = SecTrustEvaluateWithError(sec_trust, nullptr);
386+
}
387+
388+
if (policy) {
389+
CFRelease(policy);
390+
}
391+
if (sec_trust) {
392+
CFRelease(sec_trust);
393+
}
394+
if (subj_certs) {
395+
CFRelease(subj_certs);
396+
}
397+
return result;
398+
}
399+
400+
bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) {
401+
OSStatus err;
402+
403+
bool trust_evaluated = false;
404+
bool is_self_issued = isSelfIssued(cert);
405+
406+
// Evaluate user trust domain, then admin. User settings can override
407+
// admin (and both override the system domain, but we don't check that).
408+
for (const auto& trust_domain :
409+
{kSecTrustSettingsDomainUser, kSecTrustSettingsDomainAdmin}) {
410+
CFArrayRef trust_settings = nullptr;
411+
err = SecTrustSettingsCopyTrustSettings(ref, trust_domain, &trust_settings);
412+
413+
if (err != errSecSuccess && err != errSecItemNotFound) {
414+
fprintf(stderr,
415+
"ERROR: failed to copy trust settings of system certificate%d\n",
416+
err);
417+
continue;
418+
}
419+
420+
if (err == errSecSuccess && trust_settings != nullptr) {
421+
TrustStatus result =
422+
IsTrustSettingsTrustedForPolicy(trust_settings, is_self_issued);
423+
if (result != TrustStatus::UNSPECIFIED) {
424+
CFRelease(trust_settings);
425+
return result == TrustStatus::TRUSTED;
426+
}
427+
}
428+
429+
// An empty trust settings array isn’t the same as no trust settings,
430+
// where the trust_settings parameter returns NULL.
431+
// No trust-settings array means
432+
// “this certificate must be verifiable using a known trusted certificate”.
433+
if (trust_settings == nullptr && !trust_evaluated) {
434+
bool result = IsCertificateTrustValid(ref);
435+
if (result) {
436+
return true;
437+
}
438+
// no point re-evaluating this in the admin domain
439+
trust_evaluated = true;
440+
} else if (trust_settings) {
441+
CFRelease(trust_settings);
442+
}
443+
}
444+
return false;
445+
}
446+
447+
void ReadMacOSKeychainCertificates(
448+
std::vector<std::string>* system_root_certificates) {
449+
CFTypeRef search_keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef};
450+
CFTypeRef search_values[] = {
451+
kSecClassCertificate, kSecMatchLimitAll, kCFBooleanTrue};
452+
CFDictionaryRef search = CFDictionaryCreate(kCFAllocatorDefault,
453+
search_keys,
454+
search_values,
455+
3,
456+
&kCFTypeDictionaryKeyCallBacks,
457+
&kCFTypeDictionaryValueCallBacks);
458+
459+
CFArrayRef curr_anchors = nullptr;
460+
OSStatus ortn =
461+
SecItemCopyMatching(search, reinterpret_cast<CFTypeRef*>(&curr_anchors));
462+
CFRelease(search);
463+
464+
if (ortn) {
465+
fprintf(stderr, "ERROR: SecItemCopyMatching failed %d\n", ortn);
466+
}
467+
468+
CFIndex count = CFArrayGetCount(curr_anchors);
469+
470+
std::vector<X509*> system_root_certificates_X509;
471+
for (int i = 0; i < count; ++i) {
472+
SecCertificateRef cert_ref = reinterpret_cast<SecCertificateRef>(
473+
const_cast<void*>(CFArrayGetValueAtIndex(curr_anchors, i)));
474+
475+
CFDataRef der_data = SecCertificateCopyData(cert_ref);
476+
if (!der_data) {
477+
fprintf(stderr, "ERROR: SecCertificateCopyData failed\n");
478+
continue;
479+
}
480+
auto data_buffer_pointer = CFDataGetBytePtr(der_data);
481+
482+
X509* cert =
483+
d2i_X509(nullptr, &data_buffer_pointer, CFDataGetLength(der_data));
484+
CFRelease(der_data);
485+
bool is_valid = IsCertificateTrustedForPolicy(cert, cert_ref);
486+
if (is_valid) {
487+
system_root_certificates_X509.emplace_back(cert);
488+
}
489+
}
490+
CFRelease(curr_anchors);
491+
492+
for (size_t i = 0; i < system_root_certificates_X509.size(); i++) {
493+
ncrypto::X509View x509_view(system_root_certificates_X509[i]);
494+
495+
auto pem_bio = x509_view.toPEM();
496+
if (!pem_bio) {
497+
fprintf(stderr,
498+
"Warning: converting system certificate to PEM format failed\n");
499+
continue;
500+
}
501+
502+
char* pem_data = nullptr;
503+
auto pem_size = BIO_get_mem_data(pem_bio.get(), &pem_data);
504+
if (pem_size <= 0 || !pem_data) {
505+
fprintf(
506+
stderr,
507+
"Warning: cannot read PEM-encoded data from system certificate\n");
508+
continue;
509+
}
510+
std::string certificate_string_pem(pem_data, pem_size);
511+
512+
system_root_certificates->emplace_back(certificate_string_pem);
513+
}
514+
}
515+
#endif // __APPLE__
516+
517+
void ReadSystemStoreCertificates(
518+
std::vector<std::string>* system_root_certificates) {
519+
#ifdef __APPLE__
520+
ReadMacOSKeychainCertificates(system_root_certificates);
521+
#endif
522+
}
523+
524+
std::vector<std::string> getCombinedRootCertificates() {
525+
std::vector<std::string> combined_root_certs;
526+
527+
for (size_t i = 0; i < arraysize(root_certs); i++) {
528+
combined_root_certs.emplace_back(root_certs[i]);
529+
}
530+
531+
if (per_process::cli_options->use_system_ca) {
532+
ReadSystemStoreCertificates(&combined_root_certs);
533+
}
534+
535+
return combined_root_certs;
536+
}
537+
235538
X509_STORE* NewRootCertStore() {
236539
static std::vector<X509*> root_certs_vector;
237540
static bool root_certs_vector_loaded = false;
@@ -240,12 +543,17 @@ X509_STORE* NewRootCertStore() {
240543

241544
if (!root_certs_vector_loaded) {
242545
if (per_process::cli_options->ssl_openssl_cert_store == false) {
243-
for (size_t i = 0; i < arraysize(root_certs); i++) {
244-
X509* x509 = PEM_read_bio_X509(
245-
NodeBIO::NewFixed(root_certs[i], strlen(root_certs[i])).get(),
246-
nullptr, // no re-use of X509 structure
247-
NoPasswordCallback,
248-
nullptr); // no callback data
546+
std::vector<std::string> combined_root_certs =
547+
getCombinedRootCertificates();
548+
549+
for (size_t i = 0; i < combined_root_certs.size(); i++) {
550+
X509* x509 =
551+
PEM_read_bio_X509(NodeBIO::NewFixed(combined_root_certs[i].data(),
552+
combined_root_certs[i].length())
553+
.get(),
554+
nullptr, // no re-use of X509 structure
555+
NoPasswordCallback,
556+
nullptr); // no callback data
249557

250558
// Parse errors from the built-in roots are fatal.
251559
CHECK_NOT_NULL(x509);

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
11201120
,
11211121
&PerProcessOptions::use_openssl_ca,
11221122
kAllowedInEnvvar);
1123+
AddOption("--use-system-ca",
1124+
"use system's CA store",
1125+
&PerProcessOptions::use_system_ca,
1126+
kAllowedInEnvvar);
11231127
AddOption("--use-bundled-ca",
11241128
"use bundled CA store"
11251129
#if !defined(NODE_OPENSSL_CERT_STORE)

0 commit comments

Comments
 (0)