Files
node/test/parallel/test-crypto-no-algorithm.js
T
Colin McDonnell 43d3fe9558 src: seed V8 from the OS CSPRNG instead of OpenSSL's DRBG
InitializeOncePerProcessInternal() calls CSPRNG(nullptr, 0) to confirm
OpenSSL's random source is seeded and installs a V8 entropy source that
goes through CSPRNG() as well. The first RAND_status() of the process
therefore runs before V8 starts, instantiates the DRBG, and with it
constructs the default provider's algorithm and name tables
(ossl_method_construct, ossl_namemap_stored): 3.7% of the samples of
`node -e 0` on Linux x64, all of it before v8Start.

V8 uses the entropy for hash seeds, address space layout randomization
and Math.random(), none of which are cryptographic, so read the OS
CSPRNG directly through uv_random(). AIX is the exception: uv_random()
reads the blocking /dev/random there, so it stays on OpenSSL's DRBG,
which seeds from /dev/urandom.

Keep activating the default provider at startup, which the eager check
did as a side effect and --openssl-legacy-provider depends on. Its
explicit OSSL_PROVIDER_load() disables OpenSSL's provider fallback, so
without a prior activation the default provider never loads. Run the
seeding check itself only when that provider is unavailable or FIPS is
in effect, the cases where an OpenSSL configuration from any source
can leave the process without a DRBG and an early abort beats a hang
at the first crypto call. Every crypto consumer stays on OpenSSL, and
a system without a usable CSPRNG still aborts at startup, now from
uv_random() failing.

Two other behaviors change. A configuration whose [random] section
names a DRBG that cannot be fetched used to abort at startup; it now
starts and the first crypto call fails on the fetch. With --secure-heap
the process DRBGs are instantiated after the secure heap exists, so
they are allocated from it, and a Worker's isolate setup no longer
aborts the process from the entropy callback when the heap cannot hold
another per-thread DRBG. Tests cover both, and the default provider
staying active under --openssl-legacy-provider.

Measured on Linux x64 against an unpatched build of the same tree,
both binaries interleaved, min of 300 runs: `node -e 0` 29.18 ->
27.82 ms, nodeStart to v8Start 2.91 -> 2.11 ms. RAND_status and the
provider's table construction leave the startup profile (2.8% of
samples before); the provider activation that remains is 0.05%. The
first crypto.randomBytes() instantiates the DRBG in 0.19 ms. The
`parallel`, `sequential`, `message`, `es-module` and `addons` suites
show no failure the unpatched build does not have.

Refs: https://github.com/nodejs/node/commit/5cc36c39d2
Refs: https://github.com/nodejs/node/pull/44493
Refs: https://github.com/nodejs/node/pull/46237
Signed-off-by: Colin McDonnell <3084745+colinhacks@users.noreply.github.com>
PR-URL: https://github.com/nodejs/node/pull/65796
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-09-10 05:11:24 +00:00

78 lines
3.1 KiB
JavaScript

'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3))
common.skip('this test requires OpenSSL 3.x');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const { isMainThread } = require('worker_threads');
if (isMainThread) {
// TODO(richardlau): Decide if `crypto.setFips` should error if the
// provider named "fips" is not available.
crypto.setFips(1);
crypto.randomBytes(20, common.mustCall((err) => {
// crypto.randomBytes should either succeed or fail but not hang.
if (err) {
assert.match(err.message, /digital envelope routines::unsupported/);
const expected = /random number generator::unable to fetch drbg/;
assert(err.opensslErrorStack.some((msg) => expected.test(msg)),
`did not find ${expected} in ${err.opensslErrorStack}`);
}
}));
const derivations = [
['HKDF', () => crypto.hkdfSync('sha256', Buffer.alloc(32), Buffer.alloc(8),
Buffer.alloc(0), 32)],
['PBKDF2', () => crypto.pbkdf2Sync('passphrase', Buffer.alloc(16), 1000, 32,
'sha256')],
];
for (const { 0: name, 1: derive } of derivations) {
try {
derive();
} catch (err) {
assert.match(err.message, /derivation failed/);
assert.strictEqual(err.code, 'ERR_OSSL_EVP_UNSUPPORTED', `${name}: ${err.code}`);
const expected = /digital envelope routines::unsupported/;
assert(err.opensslErrorStack.some((msg) => expected.test(msg)),
`${name}: did not find ${expected} in ${err.opensslErrorStack}`);
}
}
}
{
// Startup test. Should not hang.
const fixtures = require('../common/fixtures');
const { spawnSync } = require('node:child_process');
const baseConf = fixtures.path('openssl3-conf', 'base_only.cnf');
const cp = spawnSync(process.execPath,
[ `--openssl-config=${baseConf}`, '-p', '"hello"' ],
{ encoding: 'utf8' });
assert(common.nodeProcessAborted(cp.status, cp.signal),
`process did not abort, code:${cp.status} signal:${cp.signal}`);
}
// AIX keeps OpenSSL as V8's entropy source, so a DRBG that cannot be
// fetched still aborts at startup there.
if (!common.isAIX) {
// A configuration whose random section names a DRBG that cannot be
// fetched starts normally; the first crypto call fails, without a hang.
const fixtures = require('../common/fixtures');
const { spawnSync } = require('node:child_process');
const randomConf = fixtures.path('openssl3-conf', 'random_unavailable.cnf');
const cp = spawnSync(process.execPath,
[ `--openssl-config=${randomConf}`, '-e',
'require("node:crypto").randomBytes(8)' ],
{ encoding: 'utf8' });
assert(!common.nodeProcessAborted(cp.status, cp.signal),
`process aborted, code:${cp.status} signal:${cp.signal}`);
assert.strictEqual(cp.status, 1);
assert.match(cp.stderr, /unable to fetch drbg/);
}