Commit 8f480a2df84 for nodejs

commit 8f480a2df8404f210ea31dd35b95fb7d4ae006b8
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Fri Sep 18 12:24:22 2026 +0200

    test: consolidate crypto provider cache coverage

    Exercise getCiphers(), getHashes(), getMacs() and getCurves() through
    one shared cache/FIPS driver and one snapshot fixture. Keep defensive
    copies, generation changes, rejected and idempotent toggles, and
    cross-worker invalidation consistent across the lists.

    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66108
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>

diff --git a/test/fixtures/snapshot/crypto-provider-cache.js b/test/fixtures/snapshot/crypto-provider-cache.js
new file mode 100644
index 00000000000..8596693f40a
--- /dev/null
+++ b/test/fixtures/snapshot/crypto-provider-cache.js
@@ -0,0 +1,125 @@
+'use strict';
+
+const assert = require('node:assert');
+const {
+  createHash,
+  createCipheriv,
+  createMac,
+  getCipherInfo,
+  getCiphers,
+  getCurves,
+  getHashes,
+  getMacs,
+  setFips,
+} = require('node:crypto');
+const { setDeserializeMainFunction } = require('node:v8').startupSnapshot;
+
+const cipher = 'aes-128-cbc-cts';
+const key = Buffer.alloc(16);
+const iv = Buffer.alloc(16);
+const legacyCipher = 'blowfish';
+const legacyHash = 'md4';
+const mac = 'poly1305';
+const macKey = Buffer.from(
+  '85d6be7857556d337f4452fe42d506a8' +
+  '0103808afb0db2fd4abff6af4149f51b',
+  'hex',
+);
+const macData = Buffer.from('Cryptographic Forum Research Group');
+const macExpected = 'a8061dc1305136c6c22b8baf0c0127a9';
+
+setFips(0);
+const hasMac = getMacs().includes(mac);
+const cases = [
+  { name: 'getCiphers', get: getCiphers, algorithm: cipher, legacy: legacyCipher },
+  { name: 'getHashes', get: getHashes, algorithm: 'md5', legacy: legacyHash },
+  { name: 'getMacs', get: getMacs, algorithm: hasMac ? mac : undefined },
+  { name: 'getCurves', get: getCurves, algorithm: 'secp256k1' },
+];
+
+function assertMac() {
+  if (!hasMac) return;
+  assert.strictEqual(
+    createMac(mac, macKey).update(macData).final('hex'),
+    macExpected,
+  );
+}
+
+function assertHash() {
+  assert.strictEqual(
+    createHash('md5').digest('hex'),
+    'd41d8cd98f00b204e9800998ecf8427e',
+  );
+}
+
+for (const { name, get, algorithm, legacy } of cases) {
+  const list = get();
+  if (algorithm !== undefined)
+    assert(list.includes(algorithm), `${name}: ${algorithm}`);
+  if (legacy !== undefined)
+    assert(list.includes(legacy), `${name}: ${legacy}`);
+}
+assert(getCipherInfo(cipher));
+createCipheriv(cipher, key, iv);
+createHash(legacyHash).digest();
+assertHash();
+assertMac();
+
+setDeserializeMainFunction(() => {
+  // Resolve native handles and JavaScript alias IDs before refreshing any
+  // algorithm list. Build-time caches must not survive snapshot serialization.
+  assertMac();
+  assertHash();
+  assert(getCipherInfo(cipher));
+  createCipheriv(cipher, key, iv);
+
+  const restoredLists = cases.map(({ name, get, algorithm, legacy }) => {
+    const list = get();
+    if (algorithm !== undefined)
+      assert(list.includes(algorithm), `${name}: ${algorithm}`);
+    if (legacy !== undefined)
+      assert(!list.includes(legacy), `${name}: ${legacy}`);
+
+    const disposable = get();
+    assert.notStrictEqual(disposable, list, name);
+    disposable.length = 0;
+    disposable.push('not-a-real-algorithm');
+    assert.deepStrictEqual(get(), list, name);
+    return list;
+  });
+  assertMac();
+  assert.throws(
+    () => createCipheriv(legacyCipher, key, Buffer.alloc(8)),
+    { code: 'ERR_OSSL_EVP_UNSUPPORTED' },
+  );
+  assert.throws(
+    () => createHash(legacyHash),
+    { code: 'ERR_OSSL_EVP_UNSUPPORTED' },
+  );
+
+  setFips(1);
+  for (const { name, get, algorithm } of cases) {
+    const list = get();
+    if (algorithm !== undefined)
+      assert(!list.includes(algorithm), `${name}: ${algorithm}`);
+  }
+  assert.strictEqual(getCipherInfo(cipher), undefined);
+  assert.throws(() => createCipheriv(cipher, key, iv), {
+    code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
+  });
+  if (hasMac) {
+    assert.throws(() => createMac(mac, macKey), {
+      code: 'ERR_CRYPTO_INVALID_MAC',
+    });
+  }
+
+  setFips(0);
+  for (const [index, { name, get }] of cases.entries()) {
+    assert.deepStrictEqual(get(), restoredLists[index], name);
+  }
+  assert(getCipherInfo(cipher));
+  createCipheriv(cipher, key, iv);
+  assertHash();
+  assertMac();
+  console.log('provider crypto caches snapshot: ok');
+});
diff --git a/test/fixtures/snapshot/crypto-provider-cipher-cache.js b/test/fixtures/snapshot/crypto-provider-cipher-cache.js
deleted file mode 100644
index 6d765f6b89b..00000000000
--- a/test/fixtures/snapshot/crypto-provider-cipher-cache.js
+++ /dev/null
@@ -1,55 +0,0 @@
-'use strict';
-
-const assert = require('assert');
-const {
-  createHash,
-  createCipheriv,
-  getCipherInfo,
-  getCiphers,
-  getHashes,
-  setFips,
-} = require('crypto');
-const { setDeserializeMainFunction } = require('v8').startupSnapshot;
-
-const algorithm = 'aes-128-cbc-cts';
-const key = Buffer.alloc(16);
-const iv = Buffer.alloc(16);
-const legacyCipher = 'blowfish';
-const legacyHash = 'md4';
-
-setFips(0);
-assert(getCiphers().includes(algorithm));
-assert(getCiphers().includes(legacyCipher));
-assert(getHashes().includes(legacyHash));
-assert(getCipherInfo(algorithm));
-createCipheriv(algorithm, key, iv);
-createHash(legacyHash).digest();
-
-setDeserializeMainFunction(() => {
-  assert(getCiphers().includes(algorithm));
-  assert(!getCiphers().includes(legacyCipher));
-  assert(!getHashes().includes(legacyHash));
-  assert(getCipherInfo(algorithm));
-  createCipheriv(algorithm, key, iv);
-  assert.throws(
-    () => createCipheriv(legacyCipher, key, Buffer.alloc(8)),
-    { code: 'ERR_OSSL_EVP_UNSUPPORTED' },
-  );
-  assert.throws(
-    () => createHash(legacyHash),
-    { code: 'ERR_OSSL_EVP_UNSUPPORTED' },
-  );
-
-  setFips(1);
-  assert(!getCiphers().includes(algorithm));
-  assert.strictEqual(getCipherInfo(algorithm), undefined);
-  assert.throws(() => createCipheriv(algorithm, key, iv), {
-    code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
-  });
-
-  setFips(0);
-  assert(getCiphers().includes(algorithm));
-  assert(getCipherInfo(algorithm));
-  createCipheriv(algorithm, key, iv);
-  console.log('provider crypto caches snapshot: ok');
-});
diff --git a/test/fixtures/snapshot/crypto-provider-mac-cache.js b/test/fixtures/snapshot/crypto-provider-mac-cache.js
deleted file mode 100644
index ac401d8a979..00000000000
--- a/test/fixtures/snapshot/crypto-provider-mac-cache.js
+++ /dev/null
@@ -1,65 +0,0 @@
-'use strict';
-
-const assert = require('node:assert');
-const {
-  createMac,
-  getFips,
-  getMacs,
-  setFips,
-} = require('node:crypto');
-const { setDeserializeMainFunction } = require('node:v8').startupSnapshot;
-
-const algorithm = 'poly1305';
-const key = Buffer.from(
-  '85d6be7857556d337f4452fe42d506a8' +
-  '0103808afb0db2fd4abff6af4149f51b',
-  'hex',
-);
-const data = Buffer.from('Cryptographic Forum Research Group');
-const expected = 'a8061dc1305136c6c22b8baf0c0127a9';
-
-setFips(0);
-assert(getMacs().includes(algorithm));
-assert.strictEqual(
-  createMac(algorithm, key).update(data).final('hex'),
-  expected,
-);
-
-setDeserializeMainFunction(() => {
-  // Resolve through the JavaScript alias cache before refreshing getMacs().
-  // Startup snapshot serialization must not retain the build-time cache IDs.
-  assert.strictEqual(
-    createMac(algorithm, key).update(data).final('hex'),
-    expected,
-  );
-  const expectedMacs = getMacs();
-  assert(expectedMacs.includes(algorithm));
-  const disposableMacs = getMacs();
-  disposableMacs.length = 0;
-  assert.deepStrictEqual(getMacs(), expectedMacs);
-  assert.strictEqual(
-    createMac(algorithm, key).update(data).final('hex'),
-    expected,
-  );
-
-  let toggled = false;
-  try {
-    setFips(1);
-    toggled = getFips() === 1;
-  } catch {
-    // FIPS mode is optional; snapshot cache rebuilding is still covered.
-  }
-  if (toggled && !getMacs().includes(algorithm)) {
-    assert.throws(() => createMac(algorithm, key), {
-      code: 'ERR_CRYPTO_INVALID_MAC',
-    });
-  }
-  setFips(0);
-
-  assert(getMacs().includes(algorithm));
-  assert.strictEqual(
-    createMac(algorithm, key).update(data).final('hex'),
-    expected,
-  );
-  console.log('provider MAC cache snapshot: ok');
-});
diff --git a/test/parallel/test-crypto-mac-cache-snapshot.js b/test/parallel/test-crypto-mac-cache-snapshot.js
deleted file mode 100644
index 024a4afe8c3..00000000000
--- a/test/parallel/test-crypto-mac-cache-snapshot.js
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-
-const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
-if (!hasOpenSSL(3) || isBoringSSL)
-  common.skip('this test requires OpenSSL 3 EVP_MAC support');
-
-const assert = require('node:assert');
-const { getMacs } = require('node:crypto');
-const fixtures = require('../common/fixtures');
-const tmpdir = require('../common/tmpdir');
-const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
-
-if (!getMacs().includes('poly1305'))
-  common.skip('Poly1305 is not supported');
-
-const entry = fixtures.path('snapshot', 'crypto-provider-mac-cache.js');
-const buildEnv = {
-  OPENSSL_CONF: fixtures.path(
-    'openssl3-conf', 'legacy_provider_enabled.cnf'),
-};
-const runEnv = {
-  OPENSSL_CONF: fixtures.path('openssl3-conf', 'default_only.cnf'),
-};
-
-tmpdir.refresh();
-buildSnapshot(entry, buildEnv);
-const { stdout } = runWithSnapshot(undefined, runEnv);
-assert.match(stdout, /provider MAC cache snapshot: ok/);
diff --git a/test/parallel/test-crypto-mac-cache.js b/test/parallel/test-crypto-mac-cache.js
deleted file mode 100644
index a836ef92657..00000000000
--- a/test/parallel/test-crypto-mac-cache.js
+++ /dev/null
@@ -1,307 +0,0 @@
-// Flags: --expose-internals --no-warnings
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-
-const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
-if (!hasOpenSSL(3) || isBoringSSL)
-  common.skip('this test requires OpenSSL 3 EVP_MAC support');
-
-const assert = require('node:assert');
-const { once } = require('node:events');
-const {
-  createMac,
-  getFips,
-  getMacs,
-  setFips,
-} = require('node:crypto');
-const { getMacCache } = require('internal/crypto/util');
-const { internalBinding } = require('internal/test/binding');
-const { Worker } = require('node:worker_threads');
-
-const binding = internalBinding('crypto');
-const algorithm = 'poly1305';
-const key = Buffer.from(
-  '85d6be7857556d337f4452fe42d506a8' +
-  '0103808afb0db2fd4abff6af4149f51b',
-  'hex',
-);
-const data = Buffer.from('Cryptographic Forum Research Group');
-const expected = 'a8061dc1305136c6c22b8baf0c0127a9';
-const originalFips = getFips();
-
-function getAliasId(aliases, name) {
-  const normalized = name.toLowerCase();
-  for (const [alias, id] of Object.entries(aliases)) {
-    if (alias.toLowerCase() === normalized) return id;
-  }
-  return undefined;
-}
-
-try {
-  setFips(0);
-} catch {
-  common.skip('FIPS mode cannot be disabled');
-}
-if (getFips() !== 0)
-  common.skip('FIPS mode cannot be disabled');
-
-const initialMacs = getMacs();
-if (!initialMacs.includes(algorithm))
-  common.skip(`${algorithm} is not supported`);
-
-let fipsMacs;
-let canToggleFips = false;
-const generationBeforeFipsProbe = binding.getFipsCryptoGeneration();
-try {
-  setFips(1);
-} catch {
-  // FIPS mode is optional, so the non-FIPS cache checks below still run.
-  assert.strictEqual(
-    binding.getFipsCryptoGeneration(),
-    generationBeforeFipsProbe,
-  );
-}
-if (getFips() === 1) {
-  fipsMacs = getMacs();
-  canToggleFips = true;
-}
-try {
-  setFips(0);
-} catch {
-  canToggleFips = false;
-}
-
-const generation = binding.getFipsCryptoGeneration();
-setFips(0);
-assert.strictEqual(binding.getFipsCryptoGeneration(), generation);
-
-const expectedMacs = getMacs();
-const disposableMacs = getMacs();
-assert.notStrictEqual(disposableMacs, expectedMacs);
-disposableMacs.length = 0;
-disposableMacs.push('not-a-real-mac');
-assert.deepStrictEqual(getMacs(), expectedMacs);
-
-const aliases = binding.getCachedMacAliases();
-const initialAlgorithmId = getAliasId(aliases, algorithm);
-assert.strictEqual(typeof initialAlgorithmId, 'number');
-
-const macCache = getMacCache();
-const cacheName = Object.keys(macCache).find(
-  (name) => name.toLowerCase() === algorithm,
-);
-assert(cacheName);
-const descriptor = Object.getOwnPropertyDescriptor(macCache, cacheName);
-assert(descriptor);
-assert.strictEqual(descriptor.value, initialAlgorithmId);
-const sentinel = new Error('mac cache setter');
-const throwsSentinel = (err) => err === sentinel;
-
-function installThrowingMacCacheEntry(id) {
-  Object.defineProperty(macCache, cacheName, {
-    __proto__: null,
-    configurable: true,
-    enumerable: descriptor.enumerable,
-    get() { return id; },
-    set() { throw sentinel; },
-  });
-}
-
-installThrowingMacCacheEntry(-1);
-assert.throws(() => createMac(cacheName, key), throwsSentinel);
-Object.defineProperty(macCache, cacheName, descriptor);
-
-// OpenSSL exposes two spellings for each KMAC implementation. They must map
-// to the same cached EVP_MAC rather than consume separate cache entries.
-const kmac128Id = getAliasId(aliases, 'kmac128');
-const kmac128HyphenatedId = getAliasId(aliases, 'kmac-128');
-if (kmac128Id === undefined || kmac128HyphenatedId === undefined) {
-  common.printSkipMessage('KMAC-128 aliases are not available');
-} else {
-  assert.strictEqual(kmac128Id, kmac128HyphenatedId);
-  const kmacAlgorithm = 'KMAC128';
-  const hyphenatedAlgorithm = 'KMAC-128';
-  const kmacOptions = { outputLength: 32 };
-  const kmacKey = Buffer.alloc(32, 0x42);
-  const kmacData = Buffer.from('cache alias test');
-  assert.deepStrictEqual(
-    createMac(kmacAlgorithm, kmacKey, kmacOptions)
-      .update(kmacData).final(),
-    createMac(hyphenatedAlgorithm, kmacKey, kmacOptions)
-      .update(kmacData).final(),
-  );
-  const aliasesAfterUse = binding.getCachedMacAliases();
-  assert.strictEqual(getAliasId(aliasesAfterUse, 'kmac128'), kmac128Id);
-  assert.strictEqual(
-    getAliasId(aliasesAfterUse, 'kmac-128'),
-    kmac128Id,
-  );
-}
-
-if (!canToggleFips || fipsMacs.includes(algorithm)) {
-  common.printSkipMessage('FIPS cache invalidation cannot be exercised');
-  try {
-    setFips(originalFips);
-  } catch {
-    // The process is about to exit and FIPS support is optional.
-  }
-} else {
-  const liveMac = createMac(algorithm, key).update(data);
-  const worker = new Worker(`
-    'use strict';
-    const {
-      createMac,
-      getFips,
-      getMacs,
-    } = require('node:crypto');
-    const { internalBinding } = require('internal/test/binding');
-    const { parentPort, workerData } = require('node:worker_threads');
-
-    function getAliasId(aliases, name) {
-      const normalized = name.toLowerCase();
-      for (const [alias, id] of Object.entries(aliases)) {
-        if (alias.toLowerCase() === normalized) return id;
-      }
-      return undefined;
-    }
-
-    const binding = internalBinding('crypto');
-    const key = Buffer.from(workerData.key);
-    const data = Buffer.from(workerData.data);
-    const liveMac = createMac(workerData.algorithm, key).update(data);
-    getMacs();
-    const initialAlgorithmId = getAliasId(
-      binding.getCachedMacAliases(),
-      workerData.algorithm,
-    );
-    parentPort.postMessage({
-      phase: 'warm',
-      algorithmId: initialAlgorithmId,
-      generation: binding.getFipsCryptoGeneration(),
-    });
-
-    parentPort.on('message', (phase) => {
-      if (phase === 'fips-on') {
-        let errorCode;
-        try {
-          createMac(workerData.algorithm, key);
-        } catch (error) {
-          errorCode = error.code;
-        }
-        const macs = getMacs();
-        parentPort.postMessage({
-          phase,
-          algorithmId: getAliasId(
-            binding.getCachedMacAliases(),
-            workerData.algorithm,
-          ),
-          errorCode,
-          fips: getFips(),
-          generation: binding.getFipsCryptoGeneration(),
-          hasAlgorithm: macs.includes(workerData.algorithm),
-          tag: liveMac.final('hex'),
-        });
-      } else if (phase === 'fips-off') {
-        const macs = getMacs();
-        parentPort.postMessage({
-          phase,
-          algorithmId: getAliasId(
-            binding.getCachedMacAliases(),
-            workerData.algorithm,
-          ),
-          fips: getFips(),
-          generation: binding.getFipsCryptoGeneration(),
-          hasAlgorithm: macs.includes(workerData.algorithm),
-          tag: createMac(workerData.algorithm, key)
-            .update(data).final('hex'),
-        });
-      } else {
-        parentPort.close();
-      }
-    });
-  `, {
-    eval: true,
-    workerData: { algorithm, data, key },
-  });
-  worker.on('error', common.mustNotCall());
-
-  (async () => {
-    const exitPromise = once(worker, 'exit');
-    try {
-      const [warm] = await once(worker, 'message');
-      assert.strictEqual(warm.phase, 'warm');
-      assert.strictEqual(typeof warm.algorithmId, 'number');
-      assert.strictEqual(warm.generation, generation);
-
-      installThrowingMacCacheEntry(descriptor.value);
-      try {
-        setFips(1);
-        assert.throws(() => createMac(cacheName, key), throwsSentinel);
-        installThrowingMacCacheEntry(-1);
-        assert.throws(() => createMac(cacheName, key), throwsSentinel);
-      } finally {
-        Object.defineProperty(macCache, cacheName, descriptor);
-      }
-      const enabledGeneration = binding.getFipsCryptoGeneration();
-      assert.strictEqual(enabledGeneration, generation + 1n);
-      assert.strictEqual(getFips(), 1);
-      assert(!getMacs().includes(algorithm));
-      assert.strictEqual(
-        getAliasId(binding.getCachedMacAliases(), algorithm),
-        undefined,
-      );
-      assert.throws(() => createMac(algorithm, key), {
-        code: 'ERR_CRYPTO_INVALID_MAC',
-      });
-      assert.strictEqual(liveMac.final('hex'), expected);
-
-      let responsePromise = once(worker, 'message');
-      worker.postMessage('fips-on');
-      const [enabled] = await responsePromise;
-      assert.strictEqual(enabled.phase, 'fips-on');
-      assert.strictEqual(enabled.algorithmId, undefined);
-      assert.strictEqual(enabled.errorCode, 'ERR_CRYPTO_INVALID_MAC');
-      assert.strictEqual(enabled.fips, 1);
-      assert.strictEqual(enabled.generation, enabledGeneration);
-      assert.strictEqual(enabled.hasAlgorithm, false);
-      assert.strictEqual(enabled.tag, expected);
-
-      setFips(0);
-      const disabledGeneration = binding.getFipsCryptoGeneration();
-      assert.strictEqual(disabledGeneration, enabledGeneration + 1n);
-      assert.strictEqual(getFips(), 0);
-      assert(getMacs().includes(algorithm));
-      const restoredAlgorithmId = getAliasId(
-        binding.getCachedMacAliases(),
-        algorithm,
-      );
-      assert.strictEqual(typeof restoredAlgorithmId, 'number');
-      assert.notStrictEqual(restoredAlgorithmId, initialAlgorithmId);
-      assert.strictEqual(
-        createMac(algorithm, key).update(data).final('hex'),
-        expected,
-      );
-
-      responsePromise = once(worker, 'message');
-      worker.postMessage('fips-off');
-      const [disabled] = await responsePromise;
-      assert.strictEqual(disabled.phase, 'fips-off');
-      assert.strictEqual(disabled.fips, 0);
-      assert.strictEqual(disabled.generation, disabledGeneration);
-      assert.strictEqual(disabled.hasAlgorithm, true);
-      assert.strictEqual(typeof disabled.algorithmId, 'number');
-      assert.notStrictEqual(disabled.algorithmId, warm.algorithmId);
-      assert.strictEqual(disabled.tag, expected);
-
-      worker.postMessage('done');
-      const [code] = await exitPromise;
-      assert.strictEqual(code, 0);
-    } finally {
-      if (worker.threadId !== -1) await worker.terminate();
-      setFips(originalFips);
-    }
-  })().then(common.mustCall());
-}
diff --git a/test/parallel/test-crypto-mac.js b/test/parallel/test-crypto-mac.js
index c275b87a0e6..deb2a8a3d32 100644
--- a/test/parallel/test-crypto-mac.js
+++ b/test/parallel/test-crypto-mac.js
@@ -28,20 +28,11 @@ const { Transform } = require('node:stream');

 assert.strictEqual(crypto.Mac, undefined);

-const firstMacs = getMacs();
-const secondMacs = getMacs();
+const macs = getMacs();
+assert(macs.every((name) => name === name.toLowerCase()));
+assert(macs.every((name) => !/^\d+(?:\.\d+)+$/.test(name)));

-assert.notStrictEqual(firstMacs, secondMacs);
-assert.deepStrictEqual(firstMacs, [...firstMacs].sort());
-assert.strictEqual(firstMacs.length, new Set(firstMacs).size);
-assert(firstMacs.every((name) => typeof name === 'string'));
-assert(firstMacs.every((name) => name === name.toLowerCase()));
-assert(firstMacs.every((name) => !/^\d+(?:\.\d+)+$/.test(name)));
-
-firstMacs.push('not-a-real-mac');
-assert(!getMacs().includes('not-a-real-mac'));
-
-const availableMacs = new Set(secondMacs);
+const availableMacs = new Set(macs);
 if (!availableMacs.has('hmac')) {
   common.printSkipMessage('HMAC is not available from the active providers');
 } else {
diff --git a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js b/test/parallel/test-crypto-provider-cache-snapshot.js
similarity index 76%
rename from test/parallel/test-crypto-provider-cipher-cache-snapshot.js
rename to test/parallel/test-crypto-provider-cache-snapshot.js
index 3ca57279c00..522dedfc436 100644
--- a/test/parallel/test-crypto-provider-cipher-cache-snapshot.js
+++ b/test/parallel/test-crypto-provider-cache-snapshot.js
@@ -4,16 +4,16 @@ const common = require('../common');
 if (!common.hasCrypto)
   common.skip('missing crypto');

-const assert = require('assert');
-const { hasOpenSSL } = require('../common/crypto');
+const assert = require('node:assert');
+const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
 const fixtures = require('../common/fixtures');
 const tmpdir = require('../common/tmpdir');
 const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');

-if (!hasOpenSSL(3))
+if (!hasOpenSSL(3) || isBoringSSL)
   common.skip('this test requires OpenSSL 3.x');

-const entry = fixtures.path('snapshot', 'crypto-provider-cipher-cache.js');
+const entry = fixtures.path('snapshot', 'crypto-provider-cache.js');
 const buildEnv = {
   OPENSSL_CONF: fixtures.path(
     'openssl3-conf', 'legacy_provider_enabled.cnf'),
diff --git a/test/parallel/test-crypto-provider-cache.js b/test/parallel/test-crypto-provider-cache.js
new file mode 100644
index 00000000000..4767d47dc52
--- /dev/null
+++ b/test/parallel/test-crypto-provider-cache.js
@@ -0,0 +1,364 @@
+// Flags: --expose-internals --no-warnings
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const { hasOpenSSL, isBoringSSL } = require('../common/crypto');
+if (!hasOpenSSL(3) || isBoringSSL)
+  common.skip('this test requires OpenSSL 3 provider support');
+
+const assert = require('node:assert');
+const crypto = require('node:crypto');
+const { once } = require('node:events');
+const {
+  isMainThread,
+  parentPort,
+  Worker,
+  workerData,
+} = require('node:worker_threads');
+const { getMacCache } = require('internal/crypto/util');
+const { internalBinding } = require('internal/test/binding');
+
+if (!isMainThread && !workerData?.cryptoCacheTest)
+  common.skip('crypto.setFips() is not supported in workers');
+
+const binding = internalBinding('crypto');
+const getters = ['getCiphers', 'getHashes', 'getMacs', 'getCurves'];
+const cipherAlgorithm = 'camellia-128-cbc-cts';
+const cipherKey = Buffer.alloc(16);
+const iv = Buffer.alloc(16);
+const plaintext = Buffer.alloc(32);
+const hashAlgorithm = 'md5';
+const emptyHash = 'd41d8cd98f00b204e9800998ecf8427e';
+const macAlgorithm = 'poly1305';
+const macKey = Buffer.from(
+  '85d6be7857556d337f4452fe42d506a8' +
+  '0103808afb0db2fd4abff6af4149f51b',
+  'hex',
+);
+const macData = Buffer.from('Cryptographic Forum Research Group');
+const expectedMac = 'a8061dc1305136c6c22b8baf0c0127a9';
+const curve = 'secp256k1';
+
+function checkLists() {
+  const lists = {};
+  for (const name of getters) {
+    const list = crypto[name]();
+    assert(list.every((entry) => typeof entry === 'string'), name);
+    assert.deepStrictEqual(list, [...list].sort(), name);
+    assert.strictEqual(
+      new Set(list.map((entry) => entry.toLowerCase())).size,
+      list.length,
+      name,
+    );
+    const disposable = crypto[name]();
+    assert.notStrictEqual(disposable, list, name);
+    disposable.length = 0;
+    disposable.push('not-a-real-algorithm');
+    assert.deepStrictEqual(crypto[name](), list, name);
+    lists[name] = list;
+  }
+  return lists;
+}
+
+function getAliasId(aliases, name) {
+  const normalized = name.toLowerCase();
+  for (const [alias, id] of Object.entries(aliases)) {
+    if (alias.toLowerCase() === normalized) return id;
+  }
+  return undefined;
+}
+
+function checkMacAliases() {
+  // Both spellings must refer to one cached EVP_MAC.
+  const aliases = binding.getCachedMacAliases();
+  const id = getAliasId(aliases, 'kmac128');
+  const hyphenatedId = getAliasId(aliases, 'kmac-128');
+  if (id === undefined || hyphenatedId === undefined) {
+    common.printSkipMessage('KMAC-128 aliases are not available');
+    return;
+  }
+  assert.strictEqual(id, hyphenatedId);
+  const options = { outputLength: 32 };
+  const key = Buffer.alloc(32, 0x42);
+  const data = Buffer.from('cache alias test');
+  assert.deepStrictEqual(
+    crypto.createMac('KMAC128', key, options).update(data).final(),
+    crypto.createMac('KMAC-128', key, options).update(data).final(),
+  );
+  const after = binding.getCachedMacAliases();
+  assert.strictEqual(getAliasId(after, 'kmac128'), id);
+  assert.strictEqual(getAliasId(after, 'kmac-128'), id);
+}
+
+function createFixtures(lists) {
+  const fixtures = {};
+  if (lists.getCiphers.includes(cipherAlgorithm)) {
+    const info = crypto.getCipherInfo(cipherAlgorithm);
+    assert(info);
+    assert.deepStrictEqual(
+      crypto.getCipherInfo(cipherAlgorithm.toUpperCase()), info);
+    assert.deepStrictEqual(crypto.getCipherInfo(cipherAlgorithm), info);
+    fixtures.cipher = crypto.createCipheriv(cipherAlgorithm, cipherKey, iv);
+  } else {
+    common.printSkipMessage(`${cipherAlgorithm} is not supported`);
+  }
+  for (let i = 0; i < 2; i++) {
+    assert.strictEqual(
+      crypto.getCipherInfo('node-test-unknown-provider-cipher'), undefined);
+  }
+  fixtures.hash = lists.getHashes.includes(hashAlgorithm);
+  if (lists.getMacs.includes(macAlgorithm)) {
+    fixtures.mac = crypto.createMac(macAlgorithm, macKey).update(macData);
+    fixtures.macId = getAliasId(binding.getCachedMacAliases(), macAlgorithm);
+    assert.strictEqual(typeof fixtures.macId, 'number');
+  } else {
+    common.printSkipMessage(`${macAlgorithm} is not supported`);
+  }
+  checkMacAliases();
+  if (lists.getCurves.includes(curve)) {
+    fixtures.ecdh = crypto.createECDH(curve);
+    fixtures.ecdh.generateKeys();
+    fixtures.peer = crypto.createECDH(curve).generateKeys();
+    fixtures.ecdh.computeSecret(fixtures.peer);
+  }
+  return fixtures;
+}
+
+function checkEnabled(fixtures, available) {
+  // Availability comes from a fresh environment. Exercise cached handles before
+  // refreshing the warmed JavaScript lists.
+  assert(!available.getCiphers.includes(cipherAlgorithm));
+  assert(!available.getHashes.includes(hashAlgorithm));
+  assert(!available.getCurves.includes(curve));
+  if (fixtures.cipher !== undefined) {
+    assert.strictEqual(crypto.getCipherInfo(cipherAlgorithm), undefined);
+    assert.throws(
+      () => crypto.createCipheriv(cipherAlgorithm, cipherKey, iv),
+      { code: 'ERR_CRYPTO_UNKNOWN_CIPHER' },
+    );
+    const output = Buffer.concat([
+      fixtures.cipher.update(plaintext), fixtures.cipher.final(),
+    ]);
+    assert.strictEqual(output.length, plaintext.length);
+  }
+  if (fixtures.mac !== undefined) {
+    if (!available.getMacs.includes(macAlgorithm)) {
+      assert.throws(() => crypto.createMac(macAlgorithm, macKey), {
+        code: 'ERR_CRYPTO_INVALID_MAC',
+      });
+      assert.strictEqual(
+        getAliasId(binding.getCachedMacAliases(), macAlgorithm), undefined);
+    }
+    assert.strictEqual(fixtures.mac.final('hex'), expectedMac);
+  }
+  if (fixtures.ecdh !== undefined) {
+    // Existing keys and keys installed after the transition obey current policy.
+    const error = { code: 'ERR_CRYPTO_INVALID_KEYPAIR', name: 'RangeError' };
+    assert.throws(() => fixtures.ecdh.computeSecret(fixtures.peer), error);
+    const installed = crypto.createECDH(curve);
+    installed.setPrivateKey(Buffer.from('cafebabe'.repeat(8), 'hex'));
+    assert.throws(() => installed.computeSecret(fixtures.peer), error);
+  }
+}
+
+function checkDisabled(fixtures) {
+  if (fixtures.cipher !== undefined) {
+    assert(crypto.getCipherInfo(cipherAlgorithm));
+    const cipher = crypto.createCipheriv(cipherAlgorithm, cipherKey, iv);
+    const output = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+    assert.strictEqual(output.length, plaintext.length);
+  }
+  if (fixtures.hash) {
+    assert.strictEqual(crypto.createHash(hashAlgorithm).digest('hex'), emptyHash);
+  }
+  if (fixtures.mac !== undefined) {
+    assert.strictEqual(
+      crypto.createMac(macAlgorithm, macKey).update(macData).final('hex'),
+      expectedMac,
+    );
+    const id = getAliasId(binding.getCachedMacAliases(), macAlgorithm);
+    assert.strictEqual(typeof id, 'number');
+    assert.notStrictEqual(id, fixtures.macId);
+  }
+  if (fixtures.ecdh !== undefined) fixtures.ecdh.computeSecret(fixtures.peer);
+}
+
+function setFips(value) {
+  const before = crypto.getFips();
+  const generation = binding.getFipsCryptoGeneration();
+  try {
+    crypto.setFips(value);
+  } catch (err) {
+    assert.strictEqual(crypto.getFips(), before);
+    assert.strictEqual(binding.getFipsCryptoGeneration(), generation);
+    throw err;
+  }
+  assert.strictEqual(crypto.getFips(), value);
+  assert.strictEqual(
+    binding.getFipsCryptoGeneration(),
+    generation + (before === value ? 0n : 1n),
+  );
+}
+
+// Cached IDs are written back through JavaScript properties. Setter failures
+// must propagate for both a stale ID and an uncached (-1) ID.
+function withMacCacheSetter(fixtures, toggle) {
+  if (fixtures.mac === undefined) return toggle();
+  const cache = getMacCache();
+  const name = Object.keys(cache).find(
+    (name) => name.toLowerCase() === macAlgorithm);
+  assert(name);
+  const descriptor = Object.getOwnPropertyDescriptor(cache, name);
+  assert.strictEqual(descriptor.value, fixtures.macId);
+  const sentinel = new Error('mac cache setter');
+  function install(id) {
+    Object.defineProperty(cache, name, {
+      __proto__: null,
+      configurable: true,
+      enumerable: descriptor.enumerable,
+      get() { return id; },
+      set() { throw sentinel; },
+    });
+  }
+
+  function check() {
+    assert.throws(() => crypto.createMac(name, macKey), (err) => err === sentinel);
+  }
+  try {
+    install(-1);
+    check();
+    install(descriptor.value);
+    toggle();
+    check();
+    install(-1);
+    check();
+  } finally {
+    Object.defineProperty(cache, name, descriptor);
+  }
+}
+
+if (!isMainThread) {
+  function reply(phase, lists) {
+    parentPort.postMessage({
+      phase,
+      lists,
+      fips: crypto.getFips(),
+      generation: binding.getFipsCryptoGeneration(),
+    });
+  }
+  if (workerData.listsOnly) {
+    parentPort.once('message', common.mustCall(() => {
+      reply('fresh', checkLists());
+    }));
+    parentPort.postMessage('ready');
+  } else {
+    let lists = checkLists();
+    const fixtures = createFixtures(lists);
+    assert.throws(() => setFips(1), {
+      code: 'ERR_WORKER_UNSUPPORTED_OPERATION',
+    });
+    assert.deepStrictEqual(checkLists(), lists);
+    reply('warm', lists);
+    parentPort.on('message', common.mustCallAtLeast(({ phase, available }) => {
+      if (phase === 'done') {
+        parentPort.close();
+        return;
+      }
+      if (phase === 'fips-on') checkEnabled(fixtures, available);
+      if (phase === 'fips-off') checkDisabled(fixtures);
+      const next = checkLists();
+      if (phase === 'unchanged') assert.deepStrictEqual(next, lists);
+      lists = next;
+      reply(phase, lists);
+    }));
+  }
+} else {
+  async function main() {
+    const originalFips = crypto.getFips();
+    const originalLists = checkLists();
+    try {
+      setFips(0);
+    } catch (err) {
+      if (err.code !== 'ERR_CRYPTO_FIPS_FORCED') throw err;
+      assert.deepStrictEqual(checkLists(), originalLists);
+      common.printSkipMessage('FIPS mode cannot be disabled');
+      return;
+    }
+    let worker;
+    let freshWorker;
+    try {
+      const defaultLists = checkLists();
+      const fixtures = createFixtures(defaultLists);
+      worker = new Worker(__filename, { workerData: { cryptoCacheTest: true } });
+      worker.on('error', common.mustNotCall());
+      const exitPromise = once(worker, 'exit');
+      function checkReply(message, phase, lists) {
+        assert.strictEqual(message.phase, phase);
+        assert.strictEqual(message.fips, crypto.getFips());
+        assert.strictEqual(
+          message.generation, binding.getFipsCryptoGeneration());
+        assert.deepStrictEqual(message.lists, lists);
+      }
+
+      async function exchange(phase, lists) {
+        const response = once(worker, 'message');
+        worker.postMessage({ phase, available: lists });
+        const [message] = await response;
+        checkReply(message, phase, lists);
+      }
+      const [warm] = await once(worker, 'message');
+      checkReply(warm, 'warm', defaultLists);
+
+      setFips(0);
+      assert.deepStrictEqual(checkLists(), defaultLists);
+      await exchange('unchanged', defaultLists);
+
+      // AIX uses OpenSSL entropy when initializing a worker's V8 isolate.
+      // Start it before enabling FIPS properties, which can succeed without a
+      // FIPS provider, but defer its first cache lookup until after the toggle.
+      freshWorker = new Worker(__filename, {
+        workerData: { cryptoCacheTest: true, listsOnly: true },
+      });
+      freshWorker.on('error', common.mustNotCall());
+      const freshExit = once(freshWorker, 'exit');
+      const [ready] = await once(freshWorker, 'message');
+      assert.strictEqual(ready, 'ready');
+
+      withMacCacheSetter(fixtures, () => setFips(1));
+      // A cold environment provides independent expectations for both native
+      // and JavaScript caches, including when no FIPS provider is installed.
+      const freshResponse = once(freshWorker, 'message');
+      freshWorker.postMessage('read');
+      const [fresh] = await freshResponse;
+      const [freshCode] = await freshExit;
+      assert.strictEqual(freshCode, 0);
+      const enabledLists = fresh.lists;
+      checkReply(fresh, 'fresh', enabledLists);
+      checkEnabled(fixtures, enabledLists);
+      assert.deepStrictEqual(checkLists(), enabledLists);
+      await exchange('fips-on', enabledLists);
+
+      setFips(1);
+      assert.deepStrictEqual(checkLists(), enabledLists);
+      await exchange('unchanged', enabledLists);
+
+      setFips(0);
+      checkDisabled(fixtures);
+      assert.deepStrictEqual(checkLists(), defaultLists);
+      await exchange('fips-off', defaultLists);
+
+      worker.postMessage({ phase: 'done' });
+      const [code] = await exitPromise;
+      assert.strictEqual(code, 0);
+    } finally {
+      if (freshWorker !== undefined && freshWorker.threadId !== -1)
+        await freshWorker.terminate();
+      if (worker !== undefined && worker.threadId !== -1) await worker.terminate();
+      setFips(originalFips);
+    }
+  }
+  main().then(common.mustCall());
+}
diff --git a/test/parallel/test-crypto-provider-cipher-cache.js b/test/parallel/test-crypto-provider-cipher-cache.js
deleted file mode 100644
index 3654ae98243..00000000000
--- a/test/parallel/test-crypto-provider-cipher-cache.js
+++ /dev/null
@@ -1,183 +0,0 @@
-// Flags: --expose-internals --no-warnings
-'use strict';
-
-const common = require('../common');
-if (!common.hasCrypto)
-  common.skip('missing crypto');
-
-const { hasOpenSSL } = require('../common/crypto');
-if (!hasOpenSSL(3))
-  common.skip('this test requires OpenSSL 3.x');
-
-const assert = require('assert');
-const {
-  createCipheriv,
-  getCipherInfo,
-  getCiphers,
-  getFips,
-  getHashes,
-  setFips,
-} = require('crypto');
-const { internalBinding } = require('internal/test/binding');
-const { isMainThread, Worker } = require('worker_threads');
-
-if (!isMainThread)
-  common.skip('crypto.setFips() is not supported in workers');
-
-const algorithm = 'camellia-128-cbc-cts';
-const hashAlgorithm = 'md5';
-const originalFips = getFips();
-setFips(0);
-
-if (!getCiphers().includes(algorithm)) {
-  common.skip(`${algorithm} is not supported`);
-}
-assert(getHashes().includes(hashAlgorithm));
-
-const binding = internalBinding('crypto');
-const generation = binding.getFipsCryptoGeneration();
-setFips(0);
-assert.strictEqual(binding.getFipsCryptoGeneration(), generation);
-
-const ciphers = getCiphers();
-ciphers.length = 0;
-assert(getCiphers().includes(algorithm));
-
-const info = getCipherInfo(algorithm);
-assert(info);
-assert.deepStrictEqual(getCipherInfo(algorithm.toUpperCase()), info);
-assert.deepStrictEqual(getCipherInfo(algorithm), info);
-assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined);
-assert.strictEqual(getCipherInfo('node-test-unknown-provider-cipher'), undefined);
-
-const key = Buffer.alloc(16);
-const iv = Buffer.alloc(16);
-const plaintext = Buffer.alloc(32);
-const liveCipher = createCipheriv(algorithm, key, iv);
-
-const worker = new Worker(`
-  'use strict';
-  const {
-    createHash,
-    createCipheriv,
-    getCipherInfo,
-    getCiphers,
-    getHashes,
-  } = require('crypto');
-  const { internalBinding } = require('internal/test/binding');
-  const { parentPort, workerData } = require('worker_threads');
-
-  const binding = internalBinding('crypto');
-  const key = Buffer.from(workerData.key);
-  const iv = Buffer.from(workerData.iv);
-  const plaintext = Buffer.from(workerData.plaintext);
-  const liveCipher = createCipheriv(workerData.algorithm, key, iv);
-
-  getHashes();
-  getCiphers();
-  getCipherInfo(workerData.algorithm);
-  parentPort.postMessage({
-    phase: 'warm',
-    generation: binding.getFipsCryptoGeneration(),
-  });
-
-  parentPort.on('message', (phase) => {
-    if (phase === 'fips-on') {
-      let errorCode;
-      try {
-        createCipheriv(workerData.algorithm, key, iv);
-      } catch (error) {
-        errorCode = error.code;
-      }
-      const output = Buffer.concat([
-        liveCipher.update(plaintext),
-        liveCipher.final(),
-      ]);
-      parentPort.postMessage({
-        phase,
-        errorCode,
-        generation: binding.getFipsCryptoGeneration(),
-        hasCipher: getCiphers().includes(workerData.algorithm),
-        hasHash: getHashes().includes(workerData.hashAlgorithm),
-        hasInfo: getCipherInfo(workerData.algorithm) !== undefined,
-        outputLength: output.length,
-      });
-    } else if (phase === 'fips-off') {
-      const cipher = createCipheriv(workerData.algorithm, key, iv);
-      const hash = createHash(workerData.hashAlgorithm).digest('hex');
-      const output = Buffer.concat([
-        cipher.update(plaintext),
-        cipher.final(),
-      ]);
-      parentPort.postMessage({
-        phase,
-        generation: binding.getFipsCryptoGeneration(),
-        hasHash: getHashes().includes(workerData.hashAlgorithm),
-        hasCipher: getCiphers().includes(workerData.algorithm),
-        hasInfo: getCipherInfo(workerData.algorithm) !== undefined,
-        hash,
-        outputLength: output.length,
-      });
-    } else {
-      parentPort.close();
-    }
-  });
-`, {
-  eval: true,
-  workerData: { algorithm, hashAlgorithm, key, iv, plaintext },
-});
-
-let enabledGeneration;
-worker.on('message', common.mustCall((message) => {
-  if (message.phase === 'warm') {
-    assert.strictEqual(message.generation, generation);
-
-    setFips(1);
-    enabledGeneration = binding.getFipsCryptoGeneration();
-    assert.strictEqual(enabledGeneration, generation + 1n);
-    assert(!getCiphers().includes(algorithm));
-    assert(!getHashes().includes(hashAlgorithm));
-    assert.strictEqual(getCipherInfo(algorithm), undefined);
-    assert.throws(() => createCipheriv(algorithm, key, iv), {
-      code: 'ERR_CRYPTO_UNKNOWN_CIPHER',
-    });
-
-    const output = Buffer.concat([
-      liveCipher.update(plaintext),
-      liveCipher.final(),
-    ]);
-    assert.strictEqual(output.length, plaintext.length);
-    worker.postMessage('fips-on');
-  } else if (message.phase === 'fips-on') {
-    assert.strictEqual(message.generation, enabledGeneration);
-    assert.strictEqual(message.hasCipher, false);
-    assert.strictEqual(message.hasHash, false);
-    assert.strictEqual(message.hasInfo, false);
-    assert.strictEqual(message.errorCode, 'ERR_CRYPTO_UNKNOWN_CIPHER');
-    assert.strictEqual(message.outputLength, plaintext.length);
-
-    setFips(0);
-    assert.strictEqual(
-      binding.getFipsCryptoGeneration(), enabledGeneration + 1n);
-    assert(getHashes().includes(hashAlgorithm));
-    assert(getCiphers().includes(algorithm));
-    assert(getCipherInfo(algorithm));
-    worker.postMessage('fips-off');
-  } else {
-    assert.strictEqual(message.phase, 'fips-off');
-    assert.strictEqual(
-      message.generation, binding.getFipsCryptoGeneration());
-    assert.strictEqual(message.hasCipher, true);
-    assert.strictEqual(message.hasHash, true);
-    assert.strictEqual(message.hasInfo, true);
-    assert.strictEqual(
-      message.hash,
-      'd41d8cd98f00b204e9800998ecf8427e',
-    );
-    assert.strictEqual(message.outputLength, plaintext.length);
-    worker.postMessage('done');
-    setFips(originalFips);
-  }
-}, 3));
-worker.on('error', common.mustNotCall());
-worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0)));
diff --git a/test/parallel/test-crypto-provider-hashes.js b/test/parallel/test-crypto-provider-hashes.js
index 1cf453f9b22..5e688303910 100644
--- a/test/parallel/test-crypto-provider-hashes.js
+++ b/test/parallel/test-crypto-provider-hashes.js
@@ -41,12 +41,6 @@ const {

 const hashes = getHashes();
 const lowercaseHashes = hashes.map((name) => name.toLowerCase());
-const modifiedHashes = getHashes();
-modifiedHashes.length = 0;
-
-assert.deepStrictEqual(hashes, [...hashes].sort());
-assert.deepStrictEqual(getHashes(), hashes);
-assert.strictEqual(new Set(lowercaseHashes).size, hashes.length);
 if (lowercaseHashes.includes('sha1')) {
   assert(hashes.includes('RSA-SHA1'));
 }