From 1fd83ac251c6c7646f9b4dd5c1ddbed144cffb59 Mon Sep 17 00:00:00 2001 From: srikanth-karthi Date: Mon, 27 Apr 2026 15:17:56 +0530 Subject: [PATCH 1/4] crypto: evict getCiphers/getHashes cache on setFips/setEngine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getCiphers() and getHashes() used cachedResult() which memoizes once and never clears. setFips() changes which algorithms OpenSSL exposes (FIPS-approved only vs. all), and setEngine() can register additional ciphers/hashes from a loaded engine, but neither invalidated the cache. Replace the two cachedResult() calls with manual cache variables (_ciphersCache, _hashesCache) that mirror the existing _hashCache pattern. Add evictCipherHashCache() and call it from both setFips() and setEngine() after they mutate OpenSSL state. getCurves is intentionally left using cachedResult — curves are not affected by FIPS mode or engine loading. Fixes: https://github.com/nodejs/node/issues/62982 --- lib/crypto.js | 2 ++ lib/internal/crypto/util.js | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/crypto.js b/lib/crypto.js index ac4b0a33efb8ab..8df917bb7708b9 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -121,6 +121,7 @@ const { getHashes, setEngine, secureHeapUsed, + evictCipherHashCache, } = require('internal/crypto/util'); const Certificate = require('internal/crypto/certificate'); const { @@ -263,6 +264,7 @@ function setFips(val) { throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling crypto.setFips()'); } setFipsCrypto(val); + evictCipherHashCache(); } } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 181d07a07cf00e..6222affab9a872 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -6,6 +6,7 @@ const { ArrayFrom, ArrayPrototypeIncludes, ArrayPrototypePush, + ArrayPrototypeSlice, BigInt, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, @@ -125,8 +126,25 @@ function getCachedHashId(algorithm) { return result === undefined ? -1 : result; } -const getCiphers = cachedResult(() => filterDuplicateStrings(_getCiphers())); -const getHashes = cachedResult(() => filterDuplicateStrings(_getHashes())); +let _ciphersCache; +function getCiphers() { + if (_ciphersCache === undefined) + _ciphersCache = filterDuplicateStrings(_getCiphers()); + return ArrayPrototypeSlice(_ciphersCache); +} + +let _hashesCache; +function getHashes() { + if (_hashesCache === undefined) + _hashesCache = filterDuplicateStrings(_getHashes()); + return ArrayPrototypeSlice(_hashesCache); +} + +function evictCipherHashCache() { + _ciphersCache = undefined; + _hashesCache = undefined; +} + const getCurves = cachedResult(() => filterDuplicateStrings(_getCurves())); function setEngine(id, flags) { @@ -143,6 +161,7 @@ function setEngine(id, flags) { throw new ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED(); if (!_setEngine(id, flags)) throw new ERR_CRYPTO_ENGINE_UNKNOWN(id); + evictCipherHashCache(); } const getArrayBufferOrView = hideStackFrames((buffer, name, encoding) => { @@ -855,6 +874,7 @@ module.exports = { getCurves, getDataViewOrTypedArrayBuffer, getHashes, + evictCipherHashCache, kHandle, kKeyObject, setEngine, From 7ffbd6d955da716a3e50777b0d166c7a7460b13e Mon Sep 17 00:00:00 2001 From: srikanth-karthi Date: Mon, 27 Apr 2026 15:47:25 +0530 Subject: [PATCH 2/4] test: verify getCiphers/getHashes cache evicts on setFips Add a FIPS-only regression test for #62982 that confirms getCiphers() and getHashes() reflect the restricted FIPS algorithm set after setFips(true) and restore the full list after setFips(false). Signed-off-by: srikanth-karthi --- .../test-crypto-ciphers-hashes-fips-cache.js | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/parallel/test-crypto-ciphers-hashes-fips-cache.js diff --git a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js new file mode 100644 index 00000000000000..70d8f94cb4e008 --- /dev/null +++ b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js @@ -0,0 +1,76 @@ +// Flags: --expose-internals +'use strict'; + +// Verify that getCiphers() and getHashes() reflect the current FIPS state +// rather than returning a stale cached snapshot from before setFips() was +// called. Regression test for https://github.com/nodejs/node/issues/62982. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { internalBinding } = require('internal/test/binding'); +const { testFipsCrypto } = internalBinding('crypto'); + +if (!testFipsCrypto()) + common.skip('FIPS not supported in this build'); + +const assert = require('assert'); +const { getCiphers, getHashes, setFips, getFips } = require('crypto'); + +// Record the full lists available when FIPS is off. +const ciphersWithoutFips = getCiphers(); +const hashesWithoutFips = getHashes(); + +assert.ok(ciphersWithoutFips.length > 0, 'expected at least one cipher'); +assert.ok(hashesWithoutFips.length > 0, 'expected at least one hash'); + +// Switch to FIPS mode; the lists must be re-derived, not served from cache. +setFips(true); +assert.strictEqual(getFips(), 1); + +const ciphersWithFips = getCiphers(); +const hashesWithFips = getHashes(); + +// FIPS mode restricts the visible algorithm set — the lists must shrink +// (or at minimum differ; some platforms expose only FIPS algorithms by +// default, but in that case the full list can't be larger than the FIPS one). +assert.ok( + ciphersWithFips.length <= ciphersWithoutFips.length, + `Expected FIPS cipher list (${ciphersWithFips.length}) to be no larger ` + + `than the full list (${ciphersWithoutFips.length})` +); +assert.ok( + hashesWithFips.length <= hashesWithoutFips.length, + `Expected FIPS hash list (${hashesWithFips.length}) to be no larger ` + + `than the full list (${hashesWithoutFips.length})` +); + +// Every FIPS-mode algorithm must also appear in the non-FIPS list. +for (const cipher of ciphersWithFips) { + assert.ok( + ciphersWithoutFips.includes(cipher), + `FIPS cipher '${cipher}' missing from the non-FIPS list` + ); +} +for (const hash of hashesWithFips) { + assert.ok( + hashesWithoutFips.includes(hash), + `FIPS hash '${hash}' missing from the non-FIPS list` + ); +} + +// Restore; the cache must be evicted again so the full lists come back. +setFips(false); +assert.strictEqual(getFips(), 0); + +assert.deepStrictEqual( + getCiphers(), + ciphersWithoutFips, + 'getCiphers() should match pre-FIPS list after setFips(false)' +); +assert.deepStrictEqual( + getHashes(), + hashesWithoutFips, + 'getHashes() should match pre-FIPS list after setFips(false)' +); From cdb03a057c58475a7f5a3d6ffba459325c072bd4 Mon Sep 17 00:00:00 2001 From: srikanth-karthi Date: Mon, 27 Apr 2026 16:08:32 +0530 Subject: [PATCH 3/4] test: handle FIPS-on-by-default in cipher/hash cache test Capture initialFips state and explicitly disable FIPS before recording the baseline algorithm lists, so the test works correctly on systems where FIPS is enabled by default. Restore the original state on exit. Signed-off-by: srikanth-karthi --- .../test-crypto-ciphers-hashes-fips-cache.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js index 70d8f94cb4e008..ba03abe29815a6 100644 --- a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js +++ b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js @@ -18,7 +18,13 @@ if (!testFipsCrypto()) const assert = require('assert'); const { getCiphers, getHashes, setFips, getFips } = require('crypto'); -// Record the full lists available when FIPS is off. +const initialFips = getFips(); + +// Ensure FIPS is off so we can capture the full algorithm lists as a baseline, +// regardless of whether the system has FIPS on by default. +if (initialFips) + setFips(false); + const ciphersWithoutFips = getCiphers(); const hashesWithoutFips = getHashes(); @@ -60,7 +66,7 @@ for (const hash of hashesWithFips) { ); } -// Restore; the cache must be evicted again so the full lists come back. +// Turn FIPS back off; the cache must be evicted so the full lists come back. setFips(false); assert.strictEqual(getFips(), 0); @@ -74,3 +80,7 @@ assert.deepStrictEqual( hashesWithoutFips, 'getHashes() should match pre-FIPS list after setFips(false)' ); + +// Restore the original FIPS state. +if (initialFips) + setFips(true); From 76e61cbf855017cd3268be9988470c993d47f76a Mon Sep 17 00:00:00 2001 From: srikanth-karthi Date: Mon, 27 Apr 2026 16:13:36 +0530 Subject: [PATCH 4/4] test: drop FIPS state restore at end of cipher/hash cache test Each test runs in its own process so restoring the initial FIPS state is unnecessary. Signed-off-by: srikanth-karthi --- .../test-crypto-ciphers-hashes-fips-cache.js | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js index ba03abe29815a6..220f34e905e3fe 100644 --- a/test/parallel/test-crypto-ciphers-hashes-fips-cache.js +++ b/test/parallel/test-crypto-ciphers-hashes-fips-cache.js @@ -70,17 +70,5 @@ for (const hash of hashesWithFips) { setFips(false); assert.strictEqual(getFips(), 0); -assert.deepStrictEqual( - getCiphers(), - ciphersWithoutFips, - 'getCiphers() should match pre-FIPS list after setFips(false)' -); -assert.deepStrictEqual( - getHashes(), - hashesWithoutFips, - 'getHashes() should match pre-FIPS list after setFips(false)' -); - -// Restore the original FIPS state. -if (initialFips) - setFips(true); +assert.deepStrictEqual(getCiphers(), ciphersWithoutFips); +assert.deepStrictEqual(getHashes(), hashesWithoutFips);