Skip to content

Commit 8fd32ce

Browse files
committed
feat: added support for reading certificates from macOS system store
1 parent b22c3d3 commit 8fd32ce

File tree

8 files changed

+324
-7
lines changed

8 files changed

+324
-7
lines changed

doc/api/cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2809,6 +2809,13 @@ environment variables.
28092809

28102810
See `SSL_CERT_DIR` and `SSL_CERT_FILE`.
28112811

2812+
### `--use-system-ca`
2813+
2814+
Node.js uses the trusted CA certificates present in the system store along with
2815+
the `--use-bundled-ca`, `--use-openssl-ca` options.
2816+
2817+
This option is available to macOS only.
2818+
28122819
### `--use-largepages=mode`
28132820

28142821
<!-- YAML
@@ -3227,6 +3234,7 @@ one is included in the list below.
32273234
* `--use-bundled-ca`
32283235
* `--use-largepages`
32293236
* `--use-openssl-ca`
3237+
* `--use-system-ca`
32303238
* `--v8-pool-size`
32313239
* `--watch-path`
32323240
* `--watch-preserve-output`

node.gypi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@
239239
[ 'OS=="mac"', {
240240
# linking Corefoundation is needed since certain macOS debugging tools
241241
# like Instruments require it for some features
242-
'libraries': [ '-framework CoreFoundation' ],
242+
'libraries': [ '-framework CoreFoundation -framework Security' ],
243243
'defines!': [
244244
'NODE_PLATFORM="mac"',
245245
],

src/crypto/crypto_context.cc

Lines changed: 305 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
#ifndef OPENSSL_NO_ENGINE
1919
#include <openssl/engine.h>
2020
#endif // !OPENSSL_NO_ENGINE
21+
#ifdef __APPLE__
22+
#include <CoreFoundation/CoreFoundation.h>
23+
#include <Security/Security.h>
24+
#endif
25+
2126

2227
namespace node {
2328

@@ -222,6 +227,277 @@ unsigned long LoadCertsFromFile( // NOLINT(runtime/int)
222227
}
223228
}
224229

230+
enum TrustStatus { UNSPECIFIED, TRUSTED, DISTRUSTED };
231+
232+
std::string stdStringFromCF(CFStringRef s) {
233+
if (auto fastCString = CFStringGetCStringPtr(s, kCFStringEncodingUTF8)) {
234+
return std::string(fastCString);
235+
}
236+
auto utf16length = CFStringGetLength(s);
237+
auto maxUtf8len = CFStringGetMaximumSizeForEncoding(utf16length,
238+
kCFStringEncodingUTF8);
239+
std::string converted(maxUtf8len, '\0');
240+
241+
CFStringGetCString(s, converted.data(), maxUtf8len, kCFStringEncodingUTF8);
242+
converted.resize(std::strlen(converted.data()));
243+
244+
return converted;
245+
}
246+
247+
std::string getCertIssuer(X509* cert) {
248+
ClearErrorOnReturn clearErrorOnReturn;
249+
if (cert == nullptr) return {};
250+
BIO* bio = BIO_new(BIO_s_mem());
251+
if (bio == nullptr) {
252+
return nullptr;
253+
}
254+
if (X509_NAME_print_ex(
255+
bio, X509_get_issuer_name(cert), 0, XN_FLAG_ONELINE) <=
256+
0) {
257+
return {};
258+
}
259+
260+
const int resultLen = BIO_pending(bio);
261+
char* issuer = reinterpret_cast<char *>(calloc(resultLen + 1, 1));
262+
BIO_read(bio, issuer, resultLen);
263+
BIO_free_all(bio);
264+
265+
std::string str(issuer);
266+
return str;
267+
}
268+
269+
std::string getCertSubject(X509* cert) {
270+
ClearErrorOnReturn clearErrorOnReturn;
271+
if (cert == nullptr) return {};
272+
BIO* bio = BIO_new(BIO_s_mem());
273+
if (bio == nullptr) {
274+
return nullptr;
275+
}
276+
if (X509_NAME_print_ex(
277+
bio, X509_get_subject_name(cert), 0, XN_FLAG_ONELINE) <=
278+
0) {
279+
return {};
280+
}
281+
282+
const int resultLen = BIO_pending(bio);
283+
char* issuer = reinterpret_cast<char *>(calloc(resultLen + 1, 1));
284+
BIO_read(bio, issuer, resultLen);
285+
BIO_free_all(bio);
286+
287+
std::string str(issuer);
288+
return str;
289+
}
290+
291+
bool IsSelfSigned(X509* cert) {
292+
auto issuerName = getCertIssuer(cert);
293+
auto subjectName = getCertSubject(cert);
294+
295+
if (issuerName == subjectName) {
296+
// fprintf(stderr, "Self signed\n");
297+
return true;
298+
} else {
299+
// fprintf(stderr, "NOT Self signed\n");
300+
return false;
301+
}
302+
}
303+
304+
enum TrustStatus IsTrustDictionaryTrustedForPolicy(
305+
CFDictionaryRef trust_dict
306+
) {
307+
// Trust settings may be scoped to a single application
308+
// skip as this is not supported
309+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsApplication)) {
310+
return UNSPECIFIED;
311+
}
312+
313+
// Trust settings may be scoped using policy-specific constraints. For
314+
// example, SSL trust settings might be scoped to a single hostname, or EAP
315+
// settings specific to a particular WiFi network.
316+
// As this is not presently supported, skip any policy-specific trust
317+
// settings.
318+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsPolicyString)) {
319+
return UNSPECIFIED;
320+
}
321+
322+
int trust_settings_result = kSecTrustSettingsResultTrustRoot;
323+
if (CFDictionaryContainsKey(trust_dict, kSecTrustSettingsResult)) {
324+
CFNumberRef trust_settings_result_ref = (CFNumberRef) CFDictionaryGetValue(
325+
trust_dict, kSecTrustSettingsResult);
326+
327+
CFNumberGetValue(trust_settings_result_ref, kCFNumberIntType,
328+
&trust_settings_result);
329+
330+
if (!trust_settings_result_ref) {
331+
return UNSPECIFIED;
332+
}
333+
334+
if (trust_settings_result == kSecTrustSettingsResultDeny) {
335+
// fprintf(stderr, "Returning distrusted\n");
336+
return DISTRUSTED;
337+
}
338+
// fprintf(stderr, "Checking if matches trust root\n");
339+
return trust_settings_result == kSecTrustSettingsResultTrustRoot ||
340+
trust_settings_result == kSecTrustSettingsResultTrustAsRoot ?
341+
TRUSTED : UNSPECIFIED;
342+
}
343+
344+
return UNSPECIFIED;
345+
}
346+
347+
bool IsTrustSettingsTrustedForPolicy(CFArrayRef trustSettings,
348+
bool isSelfIssued) {
349+
// The trustSettings parameter can return a valid but empty CFArrayRef.
350+
// This empty trust-settings array means “always trust this certificate”
351+
// with an overall trust setting for the certificate of
352+
// kSecTrustSettingsResultTrustRoot
353+
if (CFArrayGetCount(trustSettings) == 0) {
354+
if (isSelfIssued) {
355+
return true;
356+
}
357+
}
358+
359+
CFIndex trustSettingsCount = CFArrayGetCount(trustSettings);
360+
361+
for (CFIndex i = 0; i < trustSettingsCount ; ++i) {
362+
CFDictionaryRef trustDict = (CFDictionaryRef) CFArrayGetValueAtIndex(
363+
trustSettings, i);
364+
365+
enum TrustStatus trust = IsTrustDictionaryTrustedForPolicy(trustDict);
366+
367+
if (trust == DISTRUSTED) {
368+
return false;
369+
} else if (trust == TRUSTED) {
370+
return true;
371+
}
372+
}
373+
return false;
374+
}
375+
376+
bool IsCertificateTrustValid(SecCertificateRef ref) {
377+
SecTrustRef secTrust = nullptr;
378+
CFMutableArrayRef subjCerts = CFArrayCreateMutable(
379+
nullptr, 1, &kCFTypeArrayCallBacks);
380+
CFArraySetValueAtIndex(subjCerts, 0, ref);
381+
382+
SecPolicyRef policy = SecPolicyCreateBasicX509();
383+
OSStatus ortn = SecTrustCreateWithCertificates(subjCerts, policy, &secTrust);
384+
bool result = false;
385+
if (ortn) {
386+
/* should never happen */
387+
goto errOut;
388+
}
389+
390+
result = SecTrustEvaluateWithError(secTrust, nullptr);
391+
// fprintf(stderr, "Validation result: %s\n", result ? "true" : "false");
392+
errOut:
393+
if (policy) {
394+
CFRelease(policy);
395+
}
396+
if (secTrust) {
397+
CFRelease(secTrust);
398+
}
399+
if (subjCerts) {
400+
CFRelease(subjCerts);
401+
}
402+
return result;
403+
}
404+
405+
bool IsCertificateTrustedForPolicy(X509* cert, SecCertificateRef ref) {
406+
OSStatus err;
407+
408+
for (const auto& trust_domain :
409+
{kSecTrustSettingsDomainUser, kSecTrustSettingsDomainAdmin}) {
410+
CFArrayRef trustSettings;
411+
err = SecTrustSettingsCopyTrustSettings(ref, trust_domain, &trustSettings);
412+
413+
bool isSelfSigned = IsSelfSigned(cert);
414+
415+
if (err == errSecSuccess && trustSettings != nullptr) {
416+
return IsTrustSettingsTrustedForPolicy(trustSettings, isSelfSigned);
417+
}
418+
419+
// An empty trust settings array isn’t the same as no trust settings,
420+
// where the trustSettings parameter returns NULL.
421+
// No trust-settings array means
422+
// “this certificate must be verifiable using a known trusted certificate”.
423+
if (trustSettings == nullptr) {
424+
return IsCertificateTrustValid(ref);
425+
}
426+
}
427+
return false;
428+
}
429+
430+
void ReadMacOSKeychainCertificates(
431+
std::vector<std::string>* system_root_certificates) {
432+
CFTypeRef searchKeys[] = { kSecClass, kSecMatchLimit, kSecReturnRef };
433+
CFTypeRef searchValues[] = {
434+
kSecClassCertificate, kSecMatchLimitAll, kCFBooleanTrue };
435+
CFDictionaryRef search = CFDictionaryCreate(
436+
kCFAllocatorDefault, searchKeys, searchValues, 3,
437+
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
438+
439+
CFArrayRef currAnchors = nullptr;
440+
OSStatus ortn = SecItemCopyMatching(
441+
search,
442+
reinterpret_cast<CFTypeRef *>(&currAnchors));
443+
444+
if (ortn) {
445+
fprintf(stderr, "Failed: %d\n", ortn);
446+
}
447+
448+
CFIndex count = CFArrayGetCount(currAnchors);
449+
450+
std::vector<X509*> system_root_certificates_X509;
451+
for (int i = 0; i < count ; ++i) {
452+
SecCertificateRef certRef = (SecCertificateRef) CFArrayGetValueAtIndex(
453+
currAnchors, i);
454+
455+
CFStringRef certSummary = SecCertificateCopySubjectSummary(certRef);
456+
std::string stdCertSummary = stdStringFromCF(certSummary);
457+
458+
CFDataRef derData = SecCertificateCopyData(certRef);
459+
if (!derData) {
460+
fprintf(stderr, "ERROR: SecCertificateCopyData failed\n");
461+
continue;
462+
}
463+
auto dataBufferPointer = CFDataGetBytePtr(derData);
464+
465+
X509* cert =
466+
d2i_X509(nullptr, &dataBufferPointer, CFDataGetLength(derData));
467+
CFRelease(derData);
468+
bool isValid = IsCertificateTrustedForPolicy(cert, certRef);
469+
if (isValid) {
470+
system_root_certificates_X509.emplace_back(cert);
471+
}
472+
}
473+
474+
475+
for (size_t i = 0; i < system_root_certificates_X509.size(); i++) {
476+
BIOPointer bio(BIO_new(BIO_s_mem()));
477+
CHECK(bio);
478+
479+
BUF_MEM* mem = nullptr;
480+
int result = PEM_write_bio_X509(bio.get(),
481+
system_root_certificates_X509[i]);
482+
if (!result) {
483+
fprintf(stderr, "Warning: PEM_write_bio_X509 failed with: %d", result);
484+
continue;
485+
}
486+
487+
BIO_get_mem_ptr(bio.get(), &mem);
488+
std::string certificate_string_pem(mem->data, mem->length);
489+
490+
system_root_certificates->emplace_back(certificate_string_pem);
491+
}
492+
}
493+
494+
void ReadSystemStoreCertificates(
495+
std::vector<std::string>* system_root_certificates) {
496+
#ifdef __APPLE__
497+
ReadMacOSKeychainCertificates(system_root_certificates);
498+
#endif
499+
}
500+
225501
X509_STORE* NewRootCertStore() {
226502
static std::vector<X509*> root_certs_vector;
227503
static bool root_certs_vector_loaded = false;
@@ -230,9 +506,21 @@ X509_STORE* NewRootCertStore() {
230506

231507
if (!root_certs_vector_loaded) {
232508
if (per_process::cli_options->ssl_openssl_cert_store == false) {
509+
std::vector<std::string> combined_root_certs;
510+
511+
for (size_t i = 0; i < arraysize(root_certs); i++) {
512+
combined_root_certs.emplace_back(root_certs[i]);
513+
}
514+
515+
if (per_process::cli_options->use_system_ca) {
516+
ReadSystemStoreCertificates(&combined_root_certs);
517+
}
518+
233519
for (size_t i = 0; i < arraysize(root_certs); i++) {
234520
X509* x509 = PEM_read_bio_X509(
235-
NodeBIO::NewFixed(root_certs[i], strlen(root_certs[i])).get(),
521+
NodeBIO::NewFixed(combined_root_certs[i].data(),
522+
combined_root_certs[i].length())
523+
.get(),
236524
nullptr, // no re-use of X509 structure
237525
NoPasswordCallback,
238526
nullptr); // no callback data
@@ -282,19 +570,31 @@ X509_STORE* NewRootCertStore() {
282570

283571
void GetRootCertificates(const FunctionCallbackInfo<Value>& args) {
284572
Environment* env = Environment::GetCurrent(args);
285-
Local<Value> result[arraysize(root_certs)];
573+
std::vector<std::string> combined_root_certs;
286574

287575
for (size_t i = 0; i < arraysize(root_certs); i++) {
576+
combined_root_certs.emplace_back(root_certs[i]);
577+
}
578+
579+
if (per_process::cli_options->use_system_ca) {
580+
ReadSystemStoreCertificates(&combined_root_certs);
581+
}
582+
583+
std::vector<Local<Value>> result(combined_root_certs.size());
584+
585+
for (size_t i = 0; i < combined_root_certs.size(); i++) {
288586
if (!String::NewFromOneByte(
289587
env->isolate(),
290-
reinterpret_cast<const uint8_t*>(root_certs[i]))
291-
.ToLocal(&result[i])) {
588+
reinterpret_cast<const uint8_t*>(combined_root_certs[i].data()),
589+
v8::NewStringType::kNormal,
590+
combined_root_certs[i].size())
591+
.ToLocal(&result[i])) {
292592
return;
293593
}
294594
}
295595

296596
args.GetReturnValue().Set(
297-
Array::New(env->isolate(), result, arraysize(root_certs)));
597+
Array::New(env->isolate(), result.data(), combined_root_certs.size()));
298598
}
299599

300600
bool SecureContext::HasInstance(Environment* env, const Local<Value>& value) {

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,10 @@ PerProcessOptionsParser::PerProcessOptionsParser(
11151115
,
11161116
&PerProcessOptions::use_openssl_ca,
11171117
kAllowedInEnvvar);
1118+
AddOption("--use-system-ca",
1119+
"use system's CA store",
1120+
&PerProcessOptions::use_system_ca,
1121+
kAllowedInEnvvar);
11181122
AddOption("--use-bundled-ca",
11191123
"use bundled CA store"
11201124
#if !defined(NODE_OPENSSL_CERT_STORE)

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ class PerProcessOptions : public Options {
340340
bool ssl_openssl_cert_store = false;
341341
#endif
342342
bool use_openssl_ca = false;
343+
bool use_system_ca = false;
343344
bool use_bundled_ca = false;
344345
bool enable_fips_crypto = false;
345346
bool force_fips_crypto = false;

0 commit comments

Comments
 (0)