Repository: nodejs/node
Pull request: cli: add NODE_USE_SYSTEM_CA=1

Similar to how NODE_USE_ENV_PROXY complements --use-env-proxy, this
complements --use-system-ca. This will allow the setting to be
applied to workers individually in the future.
<!--
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: doc/api/cli.md
patch: @@ -3696,6 +3696,18 @@ specified proxy.
 This can also be enabled using the [`--use-env-proxy`][] command-line flag.
 When both are set, `--use-env-proxy` takes precedence.
 
+### `NODE_USE_SYSTEM_CA=1`
+
+<!-- YAML
+added: REPLACEME
+-->
+
+Node.js uses the trusted CA certificates present in the system store along with
+the `--use-bundled-ca` option and the `NODE_EXTRA_CA_CERTS` environment variable.
+
+This can also be enabled using the [`--use-system-ca`][] command-line flag.
+When both are set, `--use-system-ca` takes precedence.
+
 ### `NODE_V8_COVERAGE=dir`
 
 When set, Node.js will begin outputting [V8 JavaScript code coverage][] and
@@ -4025,6 +4037,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
 [`--redirect-warnings`]: #--redirect-warningsfile
 [`--require`]: #-r---require-module
 [`--use-env-proxy`]: #--use-env-proxy
+[`--use-system-ca`]: #--use-system-ca
 [`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
 [`Buffer`]: buffer.md#class-buffer
 [`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html

filename: doc/node.1
patch: @@ -844,6 +844,12 @@ This currently only affects requests sent over
 .Ar fetch() .
 Support for other built-in http and https methods is under way.
 .
+.It Ev NODE_USE_SYSTEM_CA
+Similar to
+.Fl -use-system-ca .
+Use the trusted CA certificates present in the system store, in addition to the certificates in the
+bundled Mozilla CA store and certificates from `NODE_EXTRA_CA_CERTS`.
+.
 .It Ev NODE_V8_COVERAGE Ar dir
 When set, Node.js writes JavaScript code coverage information to
 .Ar dir .

filename: src/node.cc
patch: @@ -868,6 +868,15 @@ static ExitCode InitializeNodeWithArgsInternal(
   // default value.
   V8::SetFlagsFromString("--rehash-snapshot");
 
+#if HAVE_OPENSSL
+  // TODO(joyeecheung): make this a per-env option and move the normalization
+  // into HandleEnvOptions.
+  std::string use_system_ca;
+  if (credentials::SafeGetenv("NODE_USE_SYSTEM_CA", &use_system_ca) &&
+      use_system_ca == "1") {
+    per_process::cli_options->use_system_ca = true;
+  }
+#endif  // HAVE_OPENSSL
   HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
 
   std::string node_options;

filename: test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js
patch: @@ -0,0 +1,29 @@
+'use strict';
+// This tests that NODE_USE_SYSTEM_CA environment variable works the same
+// as --use-system-ca flag by comparing certificate counts.
+
+const common = require('../common');
+if (!common.hasCrypto) common.skip('missing crypto');
+
+const tls = require('tls');
+const { spawnSyncAndExitWithoutError } = require('../common/child_process');
+
+const systemCerts = tls.getCACertificates('system');
+if (systemCerts.length === 0) {
+  common.skip('no system certificates available');
+}
+
+const { child: { stdout: expectedLength } } = spawnSyncAndExitWithoutError(process.execPath, [
+  '--use-system-ca',
+  '-p',
+  `tls.getCACertificates('default').length`,
+], {
+  env: { ...process.env, NODE_USE_SYSTEM_CA: '0' },
+});
+
+spawnSyncAndExitWithoutError(process.execPath, [
+  '-p',
+  `assert.strictEqual(tls.getCACertificates('default').length, ${expectedLength.toString()})`,
+], {
+  env: { ...process.env, NODE_USE_SYSTEM_CA: '1' },
+});

filename: test/system-ca/test-native-root-certs-env.mjs
patch: @@ -0,0 +1,56 @@
+// Env: NODE_USE_SYSTEM_CA=1
+// Same as test-native-root-certs.mjs, just testing the environment variable instead of the flag.
+
+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';
+
+if (!common.hasCrypto) {
+  common.skip('requires crypto');
+}
+
+// To run this test, the system needs to be configured to trust
+// the CA certificate first (which needs an interactive GUI approval, e.g. TouchID):
+// see the README.md in this folder for instructions on how to do this.
+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', function() {
+
+  async function setupServer(key, cert) {
+    const theServer = https.createServer({
+      key: fixtures.readKey(key),
+      cert: fixtures.readKey(cert),
+    }, handleRequest);
+    theServer.listen(0);
+    await once(theServer, 'listening');
+
+    return theServer;
+  }
+
+  let server;
+
+  beforeEach(async function() {
+    server = await setupServer('agent8-key.pem', 'agent8-cert.pem');
+  });
+
+  it('trusts a valid root certificate', async function() {
+    await fetch(`https://localhost:${server.address().port}/hello-world`);
+  });
+
+  afterEach(async function() {
+    server?.close();
+  });
+});