diff --git a/src/node.cc b/src/node.cc index a43eb28b779d..2ae48af4e355 100644 --- a/src/node.cc +++ b/src/node.cc @@ -49,6 +49,9 @@ #if HAVE_OPENSSL #include "ncrypto.h" +#if OPENSSL_VERSION_MAJOR >= 3 +#include +#endif #include "node_crypto.h" #if OPENSSL_VERSION_MAJOR >= 3 && !defined(CONF_MFLAGS_IGNORE_MISSING_FILE) // OpenSSL hides this deprecated macro under OPENSSL_NO_DEPRECATED, but the @@ -1259,15 +1262,36 @@ InitializeOncePerProcessInternal(const std::vector& args, } crypto::InstallFipsIndicatorCallback(); - // Ensure CSPRNG is properly seeded. - CHECK(ncrypto::CSPRNG(nullptr, 0)); + // Activating the default provider here keeps --openssl-legacy-provider + // working. Its explicit load disables OpenSSL's fallback, and the eager + // CSPRNG check used to activate the provider as a side effect. Only + // check the seeding when that provider is missing or FIPS is on, so a + // configuration without a DRBG still aborts at startup instead of + // hanging at the first crypto call. Otherwise the DRBG is instantiated + // on first use. +#if OPENSSL_VERSION_MAJOR >= 3 + const bool check_csprng = ncrypto::isFipsEnabled() || + !OSSL_PROVIDER_available(nullptr, "default"); +#else + const bool check_csprng = true; +#endif + if (check_csprng) { + CHECK(ncrypto::CSPRNG(nullptr, 0)); + } + // V8 uses the entropy for hash seeds, ASLR and Math.random(), none of + // it cryptographic. Going through OpenSSL would instantiate the DRBG + // and build the default provider's algorithm tables on every startup. + // V8 falls back to very weak entropy when the source fails, so abort + // instead. V8::SetEntropySource([](unsigned char* buffer, size_t length) { - // V8 falls back to very weak entropy when this function fails - // and /dev/urandom isn't available. That wouldn't be so bad if - // the entropy was only used for Math.random() but it's also used for - // hash table and address space layout randomization. Better to abort. +#ifdef _AIX + // uv_random() reads /dev/random on AIX, which blocks. OpenSSL seeds + // from /dev/urandom there. CHECK(ncrypto::CSPRNG(buffer, length)); +#else + CHECK_EQ(uv_random(nullptr, nullptr, buffer, length, 0, nullptr), 0); +#endif return true; }); #endif // !defined(OPENSSL_IS_BORINGSSL) diff --git a/test/addons/openssl-providers/test-legacy-provider-option.js b/test/addons/openssl-providers/test-legacy-provider-option.js index 5ad60dac9b86..1f01ce55a8f2 100644 --- a/test/addons/openssl-providers/test-legacy-provider-option.js +++ b/test/addons/openssl-providers/test-legacy-provider-option.js @@ -22,3 +22,6 @@ if (getFips()) { common.skip('this test cannot be run in FIPS mode'); } providers.testProviderPresent('legacy'); +// The explicit legacy load disables OpenSSL's provider fallback, so the +// default provider has to be active before it runs. +providers.testProviderPresent('default'); diff --git a/test/fixtures/openssl3-conf/random_unavailable.cnf b/test/fixtures/openssl3-conf/random_unavailable.cnf new file mode 100644 index 000000000000..a2dc8d2c9ffa --- /dev/null +++ b/test/fixtures/openssl3-conf/random_unavailable.cnf @@ -0,0 +1,7 @@ +nodejs_conf = nodejs_init + +[nodejs_init] +random = random_sect + +[random_sect] +random = NO-SUCH-DRBG diff --git a/test/parallel/test-crypto-no-algorithm.js b/test/parallel/test-crypto-no-algorithm.js index 90d19ff97fcb..174d707874ab 100644 --- a/test/parallel/test-crypto-no-algorithm.js +++ b/test/parallel/test-crypto-no-algorithm.js @@ -57,3 +57,19 @@ if (isMainThread) { assert(common.nodeProcessAborted(cp.status, cp.signal), `process did not abort, code:${cp.status} signal:${cp.signal}`); } + +{ + // 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/); +} diff --git a/test/parallel/test-crypto-secure-heap.js b/test/parallel/test-crypto-secure-heap.js index 8bd93c5281da..d7be1d5b37ef 100644 --- a/test/parallel/test-crypto-secure-heap.js +++ b/test/parallel/test-crypto-secure-heap.js @@ -61,6 +61,17 @@ if (process.argv[2] === 'child') { return; } +if (process.argv[2] === 'workers') { + // A Worker's crypto calls may run out of secure heap; that surfaces as an + // error in the Worker, never as an abort of the process. + const { Worker } = require('worker_threads'); + for (let i = 0; i < 8; i++) { + new Worker('try { require("crypto").randomBytes(4); } catch {}', + { eval: true }); + } + return; +} + const child = fork( process.argv[1], ['child'], @@ -70,6 +81,17 @@ child.on('exit', common.mustCall((code) => { assert.strictEqual(code, 0); })); +{ + const child = fork( + process.argv[1], + ['workers'], + { execArgv: ['--secure-heap=1024', '--secure-heap-min=4'] }); + child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); + })); +} + { const child = fork(fixtures.path('a.js'), { execArgv: ['--secure-heap=3', '--secure-heap-min=3'],