From d2f4c5c439243f732aed666b6b6c850661f92346 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 Jul 2026 11:47:21 +0200 Subject: [PATCH 1/2] crypto: fix Argon2 bypassing FIPS mode The private OSSL_LIB_CTX used for OSSL_set_max_threads() inherits no configuration, so Argon2 escaped FIPS mode and --openssl-config. Check availability against the default context, and create the private one only when lanes > 1. Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index bed4d48d118d51..83633de9220f80 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -2793,15 +2793,24 @@ DataPointer argon2(const Buffer& pass, return {}; } - // creates a new library context to avoid locking when running concurrently - auto ctx = DeleteFnPtr{OSSL_LIB_CTX_new()}; - if (!ctx) { - return {}; - } + // A new library context is only needed for OSSL_set_max_threads(), which is + // per-context. It inherits no configuration, so availability is checked + // against the default context, otherwise Argon2 works in FIPS mode. + DeleteFnPtr ctx; + if (lanes > 1) { + if (!DeleteFnPtr{ + EVP_KDF_fetch(nullptr, algorithm.data(), nullptr)}) { + return {}; + } - // required if threads > 1 - if (lanes > 1 && OSSL_set_max_threads(ctx.get(), lanes) != 1) { - return {}; + ctx.reset(OSSL_LIB_CTX_new()); + if (!ctx) { + return {}; + } + + if (OSSL_set_max_threads(ctx.get(), lanes) != 1) { + return {}; + } } auto kdf = DeleteFnPtr{ From a075357f3b9882ea025b3413c2d0095e764c16dc Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 27 Jul 2026 11:54:52 +0200 Subject: [PATCH 2/2] crypto: preserve OpenSSL errors from KDF failures The ncrypto KDF helpers cleared the OpenSSL error queue on return, and the traits insert their own message, which makes DeriveBitsJob skip errors->Capture(). Argon2, HKDF, PBKDF2 and scrypt failures were therefore bare Errors with no code and no opensslErrorStack. Drop the guard, which DeriveBitsJob already provides, and capture before inserting since Capture() clears the store. Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 8 ------- src/crypto/crypto_argon2.cc | 1 + src/crypto/crypto_hkdf.cc | 1 + src/crypto/crypto_pbkdf2.cc | 1 + src/crypto/crypto_scrypt.cc | 1 + test/parallel/test-crypto-argon2-job.js | 27 ++++++++++++++++------- test/parallel/test-crypto-no-algorithm.js | 18 +++++++++++++++ 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 83633de9220f80..dadf0381b58fa5 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -2642,8 +2642,6 @@ DataPointer hkdf(const Digest& md, const Buffer& info, const Buffer& salt, size_t length) { - ClearErrorOnReturn clearErrorOnReturn; - if (!checkHkdfLength(md, length) || info.len > INT_MAX || salt.len > INT_MAX) { return {}; @@ -2714,8 +2712,6 @@ DataPointer scrypt(const Buffer& pass, uint64_t p, uint64_t maxmem, size_t length) { - ClearErrorOnReturn clearErrorOnReturn; - if (pass.len > INT_MAX || salt.len > INT_MAX) { return {}; } @@ -2742,8 +2738,6 @@ DataPointer pbkdf2(const Digest& md, const Buffer& salt, uint32_t iterations, size_t length) { - ClearErrorOnReturn clearErrorOnReturn; - if (pass.len > INT_MAX || salt.len > INT_MAX || length > INT_MAX) { return {}; } @@ -2775,8 +2769,6 @@ DataPointer argon2(const Buffer& pass, const Buffer& secret, const Buffer& ad, Argon2Type type) { - ClearErrorOnReturn clearErrorOnReturn; - std::string_view algorithm; switch (type) { case Argon2Type::ARGON2I: diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index 42c5179f2b2340..ed669ba0a557d3 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -150,6 +150,7 @@ bool Argon2Traits::DeriveBits(Environment* env, config.type); if (!dp) { + errors->Capture(); errors->Insert(NodeCryptoError::ARGON2_FAILED); return false; } diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index eb40ddad41c6e3..d55d2e9a2a08bd 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -130,6 +130,7 @@ bool HKDFTraits::DeriveBits(Environment* env, }, params.length); if (!dp) { + errors->Capture(); errors->Insert(NodeCryptoError::HKDF_FAILED); return false; } diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index 5c3fc438774334..0a1c5142a2fb89 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -139,6 +139,7 @@ bool PBKDF2Traits::DeriveBits(Environment* env, params.length); if (!dp) { + errors->Capture(); errors->Insert(NodeCryptoError::PBKDF2_FAILED); return false; } diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc index 91ed9fee71f052..1260ac811df496 100644 --- a/src/crypto/crypto_scrypt.cc +++ b/src/crypto/crypto_scrypt.cc @@ -137,6 +137,7 @@ bool ScryptTraits::DeriveBits(Environment* env, params.length); if (!dp) { + errors->Capture(); errors->Insert(NodeCryptoError::SCRYPT_FAILED); return false; } diff --git a/test/parallel/test-crypto-argon2-job.js b/test/parallel/test-crypto-argon2-job.js index 34db5e4660c11c..37a0127958cf48 100644 --- a/test/parallel/test-crypto-argon2-job.js +++ b/test/parallel/test-crypto-argon2-job.js @@ -29,19 +29,31 @@ const empty = Buffer.alloc(0); // Parameters that OpenSSL's Argon2 KDF rejects. const badParams = [ - { lanes: 0, keylen: 32, memcost: 16, iter: 1 }, // lanes < 1 - { lanes: 1, keylen: 32, memcost: 0, iter: 1 }, // memcost == 0 - { lanes: 1, keylen: 32, memcost: 16, iter: 0 }, // iter == 0 + { lanes: 0, keylen: 32, memcost: 16, iter: 1, + code: 'ERR_OSSL_INVALID_THREAD_POOL_SIZE', reason: /invalid thread pool size/ }, + { lanes: 1, keylen: 32, memcost: 0, iter: 1, + code: 'ERR_OSSL_INVALID_MEMORY_SIZE', reason: /invalid memory size/ }, + { lanes: 1, keylen: 32, memcost: 16, iter: 0, + code: 'ERR_OSSL_INVALID_ITERATION_COUNT', reason: /invalid iteration count/ }, ]; -for (const { lanes, keylen, memcost, iter } of badParams) { +function assertError(err, { code, reason }) { + assert.ok(err); + assert.match(err.message, /Argon2 derivation failed/); + assert.strictEqual(err.code, code); + assert.ok(err.opensslErrorStack.some((msg) => reason.test(msg)), + `did not find ${reason} in ${err.opensslErrorStack}`); +} + +for (const params of badParams) { + const { lanes, keylen, memcost, iter } = params; + { const job = new Argon2Job( kCryptoJobSync, pass, salt, lanes, keylen, memcost, iter, empty, empty, kTypeArgon2id); const { 0: err, 1: result } = job.run(); - assert.ok(err); - assert.match(err.message, /Argon2 derivation failed/); + assertError(err, params); assert.strictEqual(result, undefined); } @@ -50,8 +62,7 @@ for (const { lanes, keylen, memcost, iter } of badParams) { kCryptoJobAsync, pass, salt, lanes, keylen, memcost, iter, empty, empty, kTypeArgon2id); job.ondone = common.mustCall((err, result) => { - assert.ok(err); - assert.match(err.message, /Argon2 derivation failed/); + assertError(err, params); assert.strictEqual(result, undefined); }); job.run(); diff --git a/test/parallel/test-crypto-no-algorithm.js b/test/parallel/test-crypto-no-algorithm.js index 96236a976a89ef..3a9473dfb165e3 100644 --- a/test/parallel/test-crypto-no-algorithm.js +++ b/test/parallel/test-crypto-no-algorithm.js @@ -26,6 +26,24 @@ if (isMainThread) { `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('secret', 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}`); + } + } } {