Files
node/test/parallel/test-crypto-provider-cache.js
T
Filip Skokan acbc3ff133 test,benchmark: drop OpenSSL 1.x-only coverage
Delete test-crypto-ecb.js, which can no longer run anywhere: Blowfish
is available only from OpenSSL's legacy provider and is absent from
BoringSSL.

The addon and cctest version guards become OPENSSL_IS_BORINGSSL checks.
Remove legacy digest-alias coverage and unreachable OpenSSL 1.x
branches. JavaScript test and benchmark gates, skip reasons, and TLS
error expectations that distinguished OpenSSL 1.x from provider-backed
OpenSSL now distinguish BoringSSL, including PKCS12 parsing, named DH
groups, private-key serialization, RSA-PSS parameters, consolidated
provider caches, FIPS availability, and empty KMAC output. Keep the CCM
alternate finalization error only for BoringSSL.

Provider-only test skips use version-neutral terminology or state the
actual minimum OpenSSL version.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/64777
Fixes: https://github.com/nodejs/node/issues/56733
Refs: https://github.com/nodejs/node/pull/64211
Refs: https://github.com/nodejs/node/pull/63966
Refs: https://github.com/nodejs/node/pull/53329
Refs: https://github.com/nodejs/node/pull/48950
Refs: https://github.com/nodejs/node/issues/45465
Refs: https://github.com/nodejs/node/issues/42827
Refs: https://github.com/nodejs/node/issues/47076
Refs: https://github.com/nodejs/node/pull/62862
Refs: https://github.com/nodejs/node/pull/38512
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Richard Lau <richard.lau@ibm.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
2026-10-25 22:36:32 +01:00

386 lines
13 KiB
JavaScript

// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { isBoringSSL } = require('../common/crypto');
if (isBoringSSL)
common.skip('this test requires OpenSSL 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 checkNamedKeygen() {
for (const name of ['ML-KEM-768', 'Ed25519', 'Ed448', 'X25519', 'X448']) {
const type = name.toLowerCase();
if (binding.isKeyAlgorithmAvailable(name)) {
const { publicKey, privateKey } = crypto.generateKeyPairSync(type);
assert.strictEqual(publicKey.asymmetricKeyType, type);
assert.strictEqual(privateKey.asymmetricKeyType, type);
} else {
const error = {
name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE',
message: `The argument 'type' must be a supported key type. Received '${type}'`,
};
assert.throws(() => crypto.generateKeyPairSync(type), error);
assert.throws(() => crypto.generateKeyPair(type, common.mustNotCall()), error);
}
}
}
function createFixtures(lists) {
checkNamedKeygen();
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) {
checkNamedKeygen();
// 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) {
checkNamedKeygen();
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());
}