Repository: nodejs/node
Pull request: crypto: load system CA certificates off thread

When --use-system-ca is enabled, load the system CA certificates eagerly off the main thread to avoid blocking the main thread when the first TLS connection is made.

Using the PR, the first tls context creation with --use-system-ca is significantly faster (tested on macOS with 8 system certificates installed):

```js
const tls = require('tls');
tls.createSecureContext();
```

```
hyperfine "./node_main --use-system-ca ./create-context.js" "out/Release/node --use-system-ca ./create-context.js" --warmup 5
Benchmark 1: ./node_main --use-system-ca ./create-context.js
  Time (mean ± σ):      53.0 ms ±   6.7 ms    [User: 28.1 ms, System: 7.4 ms]
  Range (min … max):    48.7 ms …  81.0 ms    56 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Benchmark 2: out/Release/node --use-system-ca ./create-context.js
  Time (mean ± σ):      40.8 ms ±   7.6 ms    [User: 29.1 ms, System: 7.2 ms]
  Range (min … max):    36.3 ms …  81.2 ms    74 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Summary
  out/Release/node --use-system-ca ./create-context.js ran
    1.30 ± 0.29 times faster than ./node_main --use-system-ca ./create-context.js
```

When benchmarking the first context creation standalone itself (simulating the usual pattern that the application takes a while to initialize itself before starting the first TLS connection):

```js
const tls = require('tls');

setTimeout(() => {
  const start = performance.now();
  tls.createSecureContext();
  console.log(`First TLS context created after ${performance.now() - start}ms`);
}, 300);
```

```
$ ./node_main --use-system-ca create-context.js
First TLS context created after 56.967917ms

$ out/Release/node --use-system-ca create-context.js
First TLS context created after 8.476042000000007ms
```

Refs: https://github.com/nodejs/node/issues/58990

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: src/crypto/crypto_context.cc
patch: @@ -814,6 +814,23 @@ static std::vector<X509*>& GetSystemStoreCACertificates() {
   return system_store_certs;
 }
 
+static void LoadSystemCACertificates(void* data) {
+  GetSystemStoreCACertificates();
+}
+
+static uv_thread_t system_ca_thread;
+static bool system_ca_thread_started = false;
+int LoadSystemCACertificatesOffThread() {
+  // This is only run once during the initialization of the process, so
+  // it is safe to use a static thread here.
+  int r =
+      uv_thread_create(&system_ca_thread, LoadSystemCACertificates, nullptr);
+  if (r == 0) {
+    system_ca_thread_started = true;
+  }
+  return r;
+}
+
 static std::vector<X509*> InitializeExtraCACertificates() {
   std::vector<X509*> extra_certs;
   unsigned long err = LoadCertsFromFile(  // NOLINT(runtime/int)
@@ -925,6 +942,10 @@ void CleanupCachedRootCertificates() {
       X509_free(cert);
     }
   }
+  if (system_ca_thread_started) {
+    uv_thread_join(&system_ca_thread);
+    system_ca_thread_started = false;
+  }
 }
 
 void GetBundledRootCertificates(const FunctionCallbackInfo<Value>& args) {

filename: src/crypto/crypto_util.h
patch: @@ -45,6 +45,7 @@ void InitCryptoOnce();
 void InitCrypto(v8::Local<v8::Object> target);
 
 extern void UseExtraCaCerts(std::string_view file);
+extern int LoadSystemCACertificatesOffThread();
 void CleanupCachedRootCertificates();
 
 int PasswordCallback(char* buf, int size, int rwflag, void* u);

filename: src/node.cc
patch: @@ -1200,6 +1200,20 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
       return result;
     }
 
+    if (per_process::cli_options->use_system_ca) {
+      // Load the system CA certificates eagerly off the main thread to avoid
+      // blocking the main thread when the first TLS connection is made. We
+      // don't need to wait for the thread to finish with code here, as
+      // GetSystemStoreCACertificates() has a function-local static and any
+      // actual user of it will wait for that to complete initialization.
+      int r = crypto::LoadSystemCACertificatesOffThread();
+      if (r != 0) {
+        FPrintF(
+            stderr,
+            "Warning: Failed to load system CA certificates off thread: %s\n",
+            uv_strerror(r));
+      }
+    }
     // Ensure CSPRNG is properly seeded.
     CHECK(ncrypto::CSPRNG(nullptr, 0));