Files
node/test/parallel/test-crypto-mac-cache.js
T
Filip Skokan 1d8062421e crypto: add a generic MAC API
Add getMacs() and createMac() to node:crypto as a layer on top of
OpenSSL EVP_MAC. Support incremental and streaming operations with
provider parameter validation and configurable output sizes.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65553
Refs: https://github.com/nodejs/node/issues/32433
Refs: https://github.com/nodejs/node/issues/40921
Refs: https://github.com/nodejs/node/issues/48314
Refs: https://github.com/nodejs/node/pull/32448
Refs: https://github.com/nodejs/node/pull/32477
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
2026-08-29 00:58:18 +02:00

308 lines
9.8 KiB
JavaScript

// Flags: --expose-internals --no-warnings
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL3 } = require('../common/crypto');
if (!hasOpenSSL3 || process.features.openssl_is_boringssl)
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());
}