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'));
}