Skip to content
Closed
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
2 changes: 2 additions & 0 deletions src/crypto/crypto_context.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions src/crypto/crypto_util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,17 @@ std::optional<std::string> 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};
Expand Down
4 changes: 4 additions & 0 deletions src/crypto/crypto_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<v8::Object> target);

extern void UseExtraCaCerts(std::string_view file);
Expand Down Expand Up @@ -460,6 +463,7 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork {

CryptoJob<CryptoJobTraits>* job;
ASSIGN_OR_RETURN_UNWRAP(&job, args.This());
EnsureOpenSSLNameMap();
if (job->mode() == kCryptoJobWebCrypto) {
v8::Local<v8::Promise::Resolver> resolver;
if (!v8::Promise::Resolver::New(env->context()).ToLocal(&resolver)) {
Expand Down
6 changes: 6 additions & 0 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -778,6 +781,9 @@ void Worker::StartThread(const FunctionCallbackInfo<Value>& 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.
Expand Down
56 changes: 56 additions & 0 deletions test/parallel/test-crypto-off-thread-first-use.js
Original file line number Diff line number Diff line change
@@ -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());
Loading