diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 13edcb588864..1b4802bdad25 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -991,6 +991,7 @@ void StartLoadingCertificatesOffThread( // Check again under the lock. if (!tried_cert_loading_off_thread.load()) { tried_cert_loading_off_thread.store(true); + EnsureOpenSSLNameMap(); int r = uv_thread_create(&cert_loading_thread, LoadCACertificates, nullptr); cert_loading_thread_started.store(r == 0); @@ -1011,6 +1012,7 @@ void StartLoadingCertificatesOffThread( if (!has_cached_system_root_certs.load() && !tried_system_cert_loading_off_thread.load()) { tried_system_cert_loading_off_thread.store(true); + EnsureOpenSSLNameMap(); int r = uv_thread_create( &system_cert_loading_thread, LoadSystemCACertificates, nullptr); system_cert_loading_thread_started.store(r == 0); diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index a99b4f10af24..e1cc8f019510 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -478,6 +478,17 @@ std::optional ProcessFipsOptions() { return std::nullopt; } +// OpenSSL 3 copies its legacy algorithm names into the name map on the first +// fetch, and another thread can look names up while the copy is in progress. +// Some misses are cached: with OpenSSL 3.5, the decoders cached for +// "rsassaPss" keys are then an empty set, and no RSA-PSS public key can be +// decoded for the rest of the process. Startup does no fetch, so the first one +// happens here, on the calling thread, before OpenSSL work moves to another. +void EnsureOpenSSLNameMap() { + static uv_once_t once = UV_ONCE_INIT; + uv_once(&once, [] { ncrypto::Digest::Fetch("SHA256"); }); +} + bool InitCryptoOnce(Isolate* isolate) { static uv_once_t init_once = UV_ONCE_INIT; TryCatch try_catch{isolate}; diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 00ac9f6703aa..d7e177f9ddfe 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -73,6 +73,9 @@ void InstallFipsIndicatorCallback(); bool InitCryptoOnce(v8::Isolate* isolate); void InitCryptoOnce(); +// Call before starting OpenSSL work on another thread. +void EnsureOpenSSLNameMap(); + void InitCrypto(v8::Local target); extern void UseExtraCaCerts(std::string_view file); @@ -460,6 +463,7 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { CryptoJob* job; ASSIGN_OR_RETURN_UNWRAP(&job, args.This()); + EnsureOpenSSLNameMap(); if (job->mode() == kCryptoJobWebCrypto) { v8::Local resolver; if (!v8::Promise::Resolver::New(env->context()).ToLocal(&resolver)) { diff --git a/src/node_worker.cc b/src/node_worker.cc index 308d987680d7..23d0bedf880a 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -1,5 +1,8 @@ #include "node_worker.h" #include "async_wrap-inl.h" +#if HAVE_OPENSSL +#include "crypto/crypto_util.h" +#endif // HAVE_OPENSSL #include "debug_utils-inl.h" #include "histogram-inl.h" #include "memory_tracker-inl.h" @@ -778,6 +781,9 @@ void Worker::StartThread(const FunctionCallbackInfo& args) { thread_options.stack_size = w->stack_size_; uv_thread_t* tid = &w->tid_.emplace(); // Create uv_thread_t instance +#if HAVE_OPENSSL + crypto::EnsureOpenSSLNameMap(); +#endif // HAVE_OPENSSL int ret = uv_thread_create_ex(tid, &thread_options, [](void* arg) { // XXX: This could become a std::unique_ptr, but that makes at least // gcc 6.3 detect undefined behaviour when there shouldn't be any. diff --git a/test/parallel/test-crypto-off-thread-first-use.js b/test/parallel/test-crypto-off-thread-first-use.js new file mode 100644 index 000000000000..e19165503f59 --- /dev/null +++ b/test/parallel/test-crypto-off-thread-first-use.js @@ -0,0 +1,56 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +// OpenSSL 3 builds its table of algorithm names on first use. If that first +// use happens on a thread started by Node.js (a thread pool crypto job, the +// root certificate loader or a Worker) while the main thread decodes an +// RSA-PSS key, the decode can miss the RSA-PSS name, and every later RSA-PSS +// decode in the process fails as well. + +const assert = require('assert'); +const fixtures = require('../common/fixtures'); + +if (process.argv[2] === 'child') { + const { X509Certificate, pbkdf2 } = require('crypto'); + const pem = fixtures.readKey('rsa_pss_cert_2048.pem'); + // OpenSSL must not have been used on this thread before this point. + if (process.argv[3] === 'tls') { + require('tls'); + } else if (process.argv[3] === 'worker') { + const { Worker } = require('worker_threads'); + const started = new Int32Array(new SharedArrayBuffer(4)); + new Worker(` + const { workerData } = require('worker_threads'); + const { createHash } = require('crypto'); + Atomics.store(workerData, 0, 1); + Atomics.notify(workerData, 0); + createHash('sha256'); + `, { eval: true, workerData: started }); + Atomics.wait(started, 0, 0); + } else { + pbkdf2('password', '0123456789abcdef', 1000, 32, 'sha256', + common.mustSucceed()); + } + const cert = new X509Certificate(pem); + assert.strictEqual(cert.publicKey.asymmetricKeyType, 'rsa-pss'); + return; +} + +const { hasOpenSSL } = require('../common/crypto'); +if (!hasOpenSSL(3)) + common.skip('this test requires OpenSSL 3.x'); + +(async () => { + for (const mode of ['pbkdf2', 'tls', 'worker']) { + for (let round = 0; round < 5; round++) { + const children = Array.from({ length: 4 }, () => + common.spawnPromisified(process.execPath, [__filename, 'child', mode])); + for (const { code, signal, stderr } of await Promise.all(children)) { + assert.strictEqual(signal, null, `${mode}: ${stderr}`); + assert.strictEqual(code, 0, `${mode}: ${stderr}`); + } + } + } +})().then(common.mustCall());