diff --git a/doc/api/cli.md b/doc/api/cli.md index 403fd704c32..2a85ab07e1b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -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` + + + +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 diff --git a/doc/node.1 b/doc/node.1 index 74d2d411a74..f7cb4ea0c78 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -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 . diff --git a/src/node.cc b/src/node.cc index d3bbaa40250..d6f9922a5b1 100644 --- a/src/node.cc +++ b/src/node.cc @@ -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; diff --git a/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js b/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js new file mode 100644 index 00000000000..81a5cba4da7 --- /dev/null +++ b/test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js @@ -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' }, +}); diff --git a/test/system-ca/test-native-root-certs-env.mjs b/test/system-ca/test-native-root-certs-env.mjs new file mode 100644 index 00000000000..bde7dfcd961 --- /dev/null +++ b/test/system-ca/test-native-root-certs-env.mjs @@ -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(); + }); +});