Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand Down
9 changes: 9 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions test/parallel/test-tls-get-ca-certificates-node-use-system-ca.js
Original file line number Diff line number Diff line change
@@ -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`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit... just have a preference to avoid template literals when unnecessary. but feel free to ignore.

Suggested change
`tls.getCACertificates('default').length`,
'tls.getCACertificates("default").length',

Copy link
Member Author

@joyeecheung joyeecheung Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have am impression that double quotes in an argument don't work very well on Windows. I could give it a try and see if it works though if the CI comes back happy I would prefer to use single quotes (AFAIK Windows does not do anything special with single quotes).

], {
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' },
});
56 changes: 56 additions & 0 deletions test/system-ca/test-native-root-certs-env.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Env: NODE_USE_SYSTEM_CA=1
Copy link
Member Author

@joyeecheung joyeecheung Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI @pmarchini when using this feature (thanks for implementing it by the way) I noticed that it does not yet have integration in test/common/index.js like the one for Flags so that when you run the test directly with node /path/to/test.js without the specified environment variable, the process would automatically pause the evaluation and spawn a child process to run itself with the environment variable instead.

if (process.argv.length === 2 &&
!process.env.NODE_SKIP_FLAG_CHECK &&
isMainThread &&
hasCrypto &&
require('cluster').isPrimary &&
fs.existsSync(process.argv[1])) {
const flags = parseTestFlags();

// 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();
});
});
Loading