crypto: disable non-FIPS WebCrypto paths in FIPS mode

Hide TurboSHAKE and KangarooTwelve when FIPS is enabled. Reject cSHAKE
and KMAC parameters that require implementations outside the OpenSSL
provider, while keeping provider-backed paths available.

Signed-off-by: Filip Skokan <panva.ip@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65172
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
This commit is contained in:
Filip Skokan authored and GitHub committed 2026-08-11 19:34:34 +00:00
1 parent 7d712ae426
commit ed9f464b31
20 files changed
+470 -181

No files matched your search

+4
View File
@@ -20,6 +20,7 @@ const {
normalizeHashName,
numBitsToBytes,
truncateToBitLength,
validateKmacKeyLength,
} = require('internal/crypto/util');
const {
@@ -60,6 +61,9 @@ function normalizeKeyLength(handle, algorithm) {
length = algorithm.length;
}
if (algorithm.name === 'KMAC128' || algorithm.name === 'KMAC256')
validateKmacKeyLength(length);
return { handle, length };
}
+14
View File
@@ -47,9 +47,12 @@ const {
EVP_PKEY_ML_KEM_1024,
kKeyVariantAES_OCB_128: hasAesOcbMode,
Argon2Job,
getFipsCrypto,
KmacJob,
} = internalBinding('crypto');
const isFips = getFipsCrypto() === 1;
const { getOptionValue } = require('internal/options');
const {
@@ -423,6 +426,8 @@ const conditionalAlgorithms = {
'Ed448': !process.features.openssl_is_boringssl,
'KMAC128': !!KmacJob,
'KMAC256': !!KmacJob,
'KT128': !isFips,
'KT256': !isFips,
'ML-DSA-44': !!EVP_PKEY_ML_DSA_44,
'ML-DSA-65': !!EVP_PKEY_ML_DSA_65,
'ML-DSA-87': !!EVP_PKEY_ML_DSA_87,
@@ -435,6 +440,8 @@ const conditionalAlgorithms = {
ArrayPrototypeIncludes(getHashes(), 'sha3-384'),
'SHA3-512': !process.features.openssl_is_boringssl ||
ArrayPrototypeIncludes(getHashes(), 'sha3-512'),
'TurboSHAKE128': !isFips,
'TurboSHAKE256': !isFips,
'X448': !process.features.openssl_is_boringssl,
};
@@ -579,6 +586,11 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) {
}
}
function validateKmacKeyLength(length) {
if ((length < 32 || length % 8) && isFips)
throw lazyDOMException('Invalid key length', 'NotSupportedError');
}
/**
* Converts a bit length to the number of bytes needed to contain it.
* Non-byte lengths are rounded up to the next byte.
@@ -1097,6 +1109,7 @@ module.exports = {
kNamedCurveAliases,
kSupportedAlgorithms,
isFips,
normalizeAlgorithm,
normalizeHashName,
hasAnyNotIn,
@@ -1106,6 +1119,7 @@ module.exports = {
jobPromiseThen,
cleanupWebCryptoResult,
prepareWebCryptoResult,
validateKmacKeyLength,
validateMaxBufferLength,
numBitsToBytes,
truncateToBitLength,
+34 -17
View File
@@ -9,7 +9,6 @@ const {
StringPrototypeSplit,
StringPrototypeStartsWith,
StringPrototypeToLowerCase,
TypedArrayPrototypeGetLength,
} = primordials;
const {
@@ -27,8 +26,10 @@ const {
validateMaxBufferLength,
getBufferSourceByteLength,
getBufferSourceBytes,
isFips,
kNamedCurveAliases,
numBitsToBytes,
validateKmacKeyLength,
} = require('internal/crypto/util');
const {
converters: webidl,
@@ -252,23 +253,24 @@ function validateCShakeOutputLength(V) {
}
}
function bufferSourceEqualsAscii(V, string) {
if (getBufferSourceByteLength(V) !== string.length) return false;
const bytes = getBufferSourceBytes(V);
const length = TypedArrayPrototypeGetLength(bytes);
for (let i = 0; i < length; i++) {
if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false;
}
return true;
}
const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash'];
function validateCShakeFunctionName(V) {
if (getBufferSourceByteLength(V) === 0 ||
bufferSourceEqualsAscii(V, 'KMAC') ||
bufferSourceEqualsAscii(V, 'TupleHash') ||
bufferSourceEqualsAscii(V, 'ParallelHash')) {
return;
const length = getBufferSourceByteLength(V);
if (length === 0) return;
if (!isFips) {
const bytes = getBufferSourceBytes(V);
for (let i = 0; i < kCShakeFunctionNames.length; i++) {
const functionName = kCShakeFunctionNames[i];
if (length !== functionName.length) continue;
let j = 0;
for (; j < length; j++) {
if (bytes[j] !== StringPrototypeCharCodeAt(functionName, j)) break;
}
if (j === length) return;
}
}
throw lazyDOMException(
@@ -276,6 +278,14 @@ function validateCShakeFunctionName(V) {
'NotSupportedError');
}
function validateCShakeCustomization(V) {
if (isFips && getBufferSourceByteLength(V) !== 0)
throw lazyDOMException(
'Unsupported CShakeParams customization',
'NotSupportedError');
validateMaxBufferLength(V, 'CShakeParams.customization', 512);
}
converters.RsaPssParams = createDictionaryConverter(
'RsaPssParams', [
dictAlgorithm,
@@ -433,7 +443,7 @@ converters.CShakeParams = createDictionaryConverter(
{
key: 'customization',
converter: converters.BufferSource,
validator: (V, opts) => validateMaxBufferLength(V, 'CShakeParams.customization', 512),
validator: validateCShakeCustomization,
},
],
]);
@@ -719,6 +729,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) {
key: 'length',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: validateKmacKeyLength,
},
],
]);
@@ -732,6 +743,12 @@ converters.KmacParams = createDictionaryConverter(
key: 'outputLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (V) => {
if ((V === 0 || V % 8) && isFips)
throw lazyDOMException(
'Invalid KmacParams outputLength',
'NotSupportedError');
},
required: true,
},
{
+5
View File
@@ -827,6 +827,11 @@ Maybe<void> CShakeTraits::AdditionalConfig(
CShakeConfig* params) {
Environment* env = Environment::GetCurrent(args);
if (IsFipsEnabled()) {
THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env);
return Nothing<void>();
}
CHECK(args[offset]->IsString()); // Algorithm name
Utf8Value algorithm_name(env->isolate(), args[offset]);
std::string_view algorithm_str = algorithm_name.ToStringView();
+2
View File
@@ -151,6 +151,8 @@ bool DeriveBitsWithCShake(const KmacConfig& params,
const void* key_data,
size_t key_size,
ByteSource* out) {
if (IsFipsEnabled()) return false;
const size_t key_length_bytes = NumBitsToBytes(params.key_length);
if (key_size < key_length_bytes) return false;
+10
View File
@@ -428,6 +428,11 @@ Maybe<void> TurboShakeTraits::AdditionalConfig(
TurboShakeConfig* params) {
Environment* env = Environment::GetCurrent(args);
if (IsFipsEnabled()) {
THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env);
return Nothing<void>();
}
// args[offset + 0] = algorithm name (string)
CHECK(args[offset]->IsString());
Utf8Value algorithm_name(env->isolate(), args[offset]);
@@ -535,6 +540,11 @@ Maybe<void> KangarooTwelveTraits::AdditionalConfig(
KangarooTwelveConfig* params) {
Environment* env = Environment::GetCurrent(args);
if (IsFipsEnabled()) {
THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env);
return Nothing<void>();
}
// args[offset + 0] = algorithm name (string)
CHECK(args[offset]->IsString());
Utf8Value algorithm_name(env->isolate(), args[offset]);
+6 -2
View File
@@ -146,6 +146,11 @@ bool InitCryptoOnce(Isolate* isolate) {
// be part of a larger mutex for global OpenSSL state.
static Mutex fips_mutex;
bool IsFipsEnabled() {
Mutex::ScopedLock fips_lock(fips_mutex);
return ncrypto::isFipsEnabled();
}
void InitCryptoOnce() {
Mutex::ScopedLock lock(per_process::cli_options_mutex);
Mutex::ScopedLock fips_lock(fips_mutex);
@@ -223,8 +228,7 @@ void InitCryptoOnce() {
void GetFipsCrypto(const FunctionCallbackInfo<Value>& args) {
Mutex::ScopedLock lock(per_process::cli_options_mutex);
Mutex::ScopedLock fips_lock(fips_mutex);
args.GetReturnValue().Set(ncrypto::isFipsEnabled() ? 1 : 0);
args.GetReturnValue().Set(IsFipsEnabled() ? 1 : 0);
}
void SetFipsCrypto(const FunctionCallbackInfo<Value>& args) {
+1
View File
@@ -66,6 +66,7 @@ constexpr T NumBitsToBytes(T bits) {
// what went wrong, or std::nullopt when there was nothing to do or the
// options were applied successfully.
std::optional<std::string> ProcessFipsOptions();
bool IsFipsEnabled();
bool InitCryptoOnce(v8::Isolate* isolate);
void InitCryptoOnce();
@@ -14,6 +14,7 @@ const {
} = require('crypto');
const { hasFIPS } = require('../common/crypto');
const { kSupportedAlgorithms } = require('internal/crypto/util');
const fips = hasFIPS();
const rejectsXCurves = hasFIPS(3, 5);
const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => {
@@ -142,7 +143,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) {
const key = createSecretKey(randomBytes(32));
const usages = ['sign', 'verify'];
if (allowZeroKey) {
if (allowZeroKey && !fips) {
const zeroKey = createSecretKey(Buffer.alloc(0))
.toCryptoKey(algorithm, true, usages);
assert.strictEqual(zeroKey.algorithm.length, 0);
@@ -150,6 +151,16 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) {
const explicitZeroKey = createSecretKey(Buffer.alloc(0))
.toCryptoKey({ ...algorithm, length: 0 }, true, usages);
assert.strictEqual(explicitZeroKey.algorithm.length, 0);
} else if (allowZeroKey) {
for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) {
assert.throws(() => {
createSecretKey(Buffer.alloc(0))
.toCryptoKey(zeroAlgorithm, true, usages);
}, {
name: 'NotSupportedError',
message: 'Invalid key length',
});
}
} else {
assert.throws(() => {
createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages);
@@ -164,12 +175,15 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) {
message: 'Usages cannot be empty when importing a secret key.'
});
assert.throws(() => {
key.toCryptoKey({ ...algorithm, length: 0 }, true, usages);
}, {
name: 'DataError',
message: invalidLengthMessage,
});
assert.throws(
() => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages),
allowZeroKey && fips ? {
name: 'NotSupportedError',
message: 'Invalid key length',
} : {
name: 'DataError',
message: invalidLengthMessage,
});
}
function hmacVectors() {
+2 -6
View File
@@ -284,7 +284,7 @@ const fips4 = hasFIPS(4);
})().then(common.mustCall());
}
if (hasOpenSSL(3)) {
if (hasOpenSSL(3) && !hasFIPS()) {
(async () => {
const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 };
const usages = ['sign'];
@@ -326,11 +326,7 @@ if (hasOpenSSL(3)) {
name: 'KMAC128',
outputLength: 256,
}, derived, new Uint8Array());
if (fips4) {
await assert.rejects(signature, { name: 'OperationError' });
} else {
assert.strictEqual((await signature).byteLength, 32);
}
assert.strictEqual((await signature).byteLength, 32);
}
})().then(common.mustCall());
}
@@ -5,6 +5,11 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasFIPS } = require('../common/crypto');
if (hasFIPS())
common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode');
const assert = require('assert');
const { subtle } = globalThis.crypto;
@@ -5,6 +5,11 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasFIPS } = require('../common/crypto');
if (hasFIPS())
common.skip('TurboSHAKE and KangarooTwelve are not available in FIPS mode');
const assert = require('assert');
const { subtle } = globalThis.crypto;
+14 -28
View File
@@ -10,7 +10,7 @@ const { Buffer } = require('buffer');
const { subtle } = globalThis.crypto;
const { createHash, getHashes } = require('crypto');
const { hasOpenSSL, hasFIPS } = require('../common/crypto');
const fips4 = hasFIPS(4);
const fips = hasFIPS();
const kTests = [
['SHA-1', ['sha1'], 160],
@@ -291,6 +291,8 @@ if (getHashes().includes('shake128')) {
message: 'Unsupported CShakeParams functionName',
});
if (fips) return;
await assert.rejects(
subtle.digest(
{
@@ -398,35 +400,19 @@ if (getHashes().includes('shake128')) {
'ca6f88db415829',
},
]) {
const digest = subtle.digest(algorithm, data);
if (fips4) {
await assert.rejects(
digest,
(err) => err.name === 'OperationError' &&
err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED');
} else {
assert.strictEqual(
Buffer.from(await digest).toString('hex'),
expected);
}
assert.strictEqual(
Buffer.from(await subtle.digest(algorithm, data)).toString('hex'),
expected);
}
const truncatedDigest = subtle.digest(
const truncated = Buffer.from(await subtle.digest(
{ ...nistCShakeSample1.algorithm, outputLength: 255 },
nistCShakeSample1.data);
if (fips4) {
await assert.rejects(
truncatedDigest,
(err) => err.name === 'OperationError' &&
err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED');
} else {
const truncated = Buffer.from(await truncatedDigest);
const expected = Buffer.from(nistCShakeSample1.expected, 'hex');
assert.strictEqual(truncated.byteLength, expected.byteLength);
assert.deepStrictEqual(
truncated.subarray(0, 31), expected.subarray(0, 31));
assert.strictEqual(truncated[31] & 0b00000001, 0);
assert.strictEqual(truncated[31] | 0b00000001, expected[31]);
}
nistCShakeSample1.data));
const expected = Buffer.from(nistCShakeSample1.expected, 'hex');
assert.strictEqual(truncated.byteLength, expected.byteLength);
assert.deepStrictEqual(
truncated.subarray(0, 31), expected.subarray(0, 31));
assert.strictEqual(truncated[31] & 0b00000001, 0);
assert.strictEqual(truncated[31] | 0b00000001, expected[31]);
})().then(common.mustCall());
}
+55 -53
View File
@@ -286,66 +286,68 @@ if (hasOpenSSL(3)) {
{ name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' });
{
const importedZeroImplicit = await subtle.importKey(
'raw-secret',
new Uint8Array(),
name,
true,
['sign', 'verify']);
const importedZeroImplicitRaw =
await subtle.exportKey('raw-secret', importedZeroImplicit);
assert.strictEqual(importedZeroImplicit.algorithm.length, 0);
assert.strictEqual(importedZeroImplicitRaw.byteLength, 0);
const importedZeroExplicit = await subtle.importKey(
'raw-secret',
new Uint8Array(),
{ name, length: 0 },
true,
['sign', 'verify']);
const importedZeroExplicitRaw =
await subtle.exportKey('raw-secret', importedZeroExplicit);
assert.strictEqual(importedZeroExplicit.algorithm.length, 0);
assert.strictEqual(importedZeroExplicitRaw.byteLength, 0);
await assert.rejects(
subtle.importKey(
if (getFips() !== 1) {
const importedZeroImplicit = await subtle.importKey(
'raw-secret',
new Uint8Array([0xff]),
new Uint8Array(),
name,
true,
['sign', 'verify']);
const importedZeroImplicitRaw =
await subtle.exportKey('raw-secret', importedZeroImplicit);
assert.strictEqual(importedZeroImplicit.algorithm.length, 0);
assert.strictEqual(importedZeroImplicitRaw.byteLength, 0);
const importedZeroExplicit = await subtle.importKey(
'raw-secret',
new Uint8Array(),
{ name, length: 0 },
true,
['sign', 'verify']),
{ name: 'DataError', message: 'Invalid key length' });
['sign', 'verify']);
const importedZeroExplicitRaw =
await subtle.exportKey('raw-secret', importedZeroExplicit);
assert.strictEqual(importedZeroExplicit.algorithm.length, 0);
assert.strictEqual(importedZeroExplicitRaw.byteLength, 0);
const generated = await subtle.generateKey(
{ name, length: 9 },
true,
['sign', 'verify']);
const generatedRaw = await subtle.exportKey('raw-secret', generated);
assert.strictEqual(generated.algorithm.length, 9);
assert.strictEqual(generatedRaw.byteLength, 2);
assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0);
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array([0xff]),
{ name, length: 0 },
true,
['sign', 'verify']),
{ name: 'DataError', message: 'Invalid key length' });
const importedExplicit = await subtle.importKey(
'raw-secret',
new Uint8Array([0xff, 0xff]),
{ name, length: 9 },
true,
['sign', 'verify']);
const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit);
assert.strictEqual(importedExplicit.algorithm.length, 9);
assert.deepStrictEqual(
new Uint8Array(importedExplicitRaw),
new Uint8Array([0xff, 0x80]));
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array([0xff]),
const generated = await subtle.generateKey(
{ name, length: 9 },
true,
['sign', 'verify']),
{ name: 'DataError', message: 'Invalid key length' });
['sign', 'verify']);
const generatedRaw = await subtle.exportKey('raw-secret', generated);
assert.strictEqual(generated.algorithm.length, 9);
assert.strictEqual(generatedRaw.byteLength, 2);
assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0);
const importedExplicit = await subtle.importKey(
'raw-secret',
new Uint8Array([0xff, 0xff]),
{ name, length: 9 },
true,
['sign', 'verify']);
const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit);
assert.strictEqual(importedExplicit.algorithm.length, 9);
assert.deepStrictEqual(
new Uint8Array(importedExplicitRaw),
new Uint8Array([0xff, 0x80]));
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array([0xff]),
{ name, length: 9 },
true,
['sign', 'verify']),
{ name: 'DataError', message: 'Invalid key length' });
}
}
}
@@ -0,0 +1,198 @@
// Flags: --expose-internals
import * as common from '../common/index.mjs';
import assert from 'node:assert';
import { createRequire } from 'node:module';
import { hasFIPS } from '../common/crypto.js';
if (!common.hasCrypto)
common.skip('missing crypto');
if (!hasFIPS(3))
common.skip('requires OpenSSL >= 3 in FIPS mode');
const require = createRequire(import.meta.url);
const { internalBinding } = require('internal/test/binding');
const { getCryptoKeyHandle } = require('internal/crypto/keys');
const {
CShakeJob,
KangarooTwelveJob,
KmacJob,
TurboShakeJob,
kCryptoJobWebCrypto,
kSignJobModeSign,
} = internalBinding('crypto');
const { subtle } = globalThis.crypto;
const { SubtleCrypto } = globalThis;
const data = new Uint8Array();
async function assertFipsException(operation, algorithm, fn, message) {
assert.strictEqual(SubtleCrypto.supports(operation, algorithm), false);
await assert.rejects(fn(), {
name: 'NotSupportedError',
message,
});
}
for (const algorithm of [
{ name: 'turboshake128', outputLength: 128 },
{ name: 'TurboSHAKE256', outputLength: 256 },
{ name: 'KT128', outputLength: 128 },
{ name: 'KT256', outputLength: 256, customization: data },
]) {
await assertFipsException(
'digest',
algorithm,
() => subtle.digest(algorithm, data),
'Unrecognized algorithm name');
}
for (const createJob of [
() => new TurboShakeJob(
kCryptoJobWebCrypto, 'TurboSHAKE128', 0x1f, 16, data),
() => new KangarooTwelveJob(
kCryptoJobWebCrypto, 'KT128', undefined, 16, data),
() => new CShakeJob(
kCryptoJobWebCrypto,
'cSHAKE128',
data,
Buffer.from('KMAC'),
undefined,
128),
]) {
assert.throws(createJob, {
code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION',
message: 'Unsupported crypto operation',
});
}
const emptyCShake = {
name: 'cSHAKE128',
outputLength: 256,
customization: data,
functionName: data,
};
assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true);
for (const length of [1, 513]) {
const algorithm = {
name: 'cSHAKE128',
outputLength: 256,
customization: new Uint8Array(length),
};
await assertFipsException(
'digest',
algorithm,
() => subtle.digest(algorithm, data),
'Unsupported CShakeParams customization');
}
const functionName = {
name: 'cSHAKE256',
outputLength: 256,
functionName: Buffer.from('KMAC'),
};
await assertFipsException(
'digest',
functionName,
() => subtle.digest(functionName, data),
'Unsupported CShakeParams functionName');
const bothCShakeParams = {
...functionName,
customization: new Uint8Array(1),
};
await assertFipsException(
'digest',
bothCShakeParams,
() => subtle.digest(bothCShakeParams, data),
'Unsupported CShakeParams customization');
for (const length of [0, 24, 33]) {
const algorithm = { name: 'KMAC128', length };
await assertFipsException(
'generateKey',
algorithm,
() => subtle.generateKey(algorithm, false, ['sign', 'verify']),
'Invalid key length');
await assertFipsException(
'importKey',
algorithm,
() => subtle.importKey(
'raw-secret',
new Uint8Array(length === 24 ? 4 : Math.ceil(length / 8)),
algorithm,
false,
['sign', 'verify']),
'Invalid key length');
}
const minimumKmac = { name: 'KMAC128', length: 32 };
assert.strictEqual(
SubtleCrypto.supports('generateKey', minimumKmac), true);
assert.strictEqual(
SubtleCrypto.supports('importKey', minimumKmac), true);
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array(5),
minimumKmac,
false,
['sign', 'verify']), {
name: 'DataError',
message: 'Invalid key length',
});
for (const length of [0, 3]) {
await assert.rejects(
subtle.importKey(
'raw-secret',
new Uint8Array(length),
'KMAC128',
false,
['sign', 'verify']), {
name: 'NotSupportedError',
message: 'Invalid key length',
});
}
const key = await subtle.importKey(
'raw-secret',
new Uint8Array(4),
'KMAC128',
false,
['sign', 'verify']);
assert.strictEqual(key.algorithm.length, 32);
await assert.rejects(
new KmacJob(
kCryptoJobWebCrypto,
kSignJobModeSign,
getCryptoKeyHandle(key),
'KMAC128',
undefined,
32,
9,
data,
undefined).run(),
(err) => {
assert.strictEqual(err.name, 'OperationError');
assert.strictEqual(err.cause?.code, 'ERR_CRYPTO_OPERATION_FAILED');
return true;
});
const minimumOutput = { name: 'KMAC128', outputLength: 8 };
assert.strictEqual(SubtleCrypto.supports('sign', minimumOutput), true);
assert.strictEqual(SubtleCrypto.supports('verify', minimumOutput), true);
for (const outputLength of [0, 9]) {
const algorithm = { name: 'KMAC128', outputLength };
await assertFipsException(
'sign',
algorithm,
() => subtle.sign(algorithm, key, data),
'Invalid KmacParams outputLength');
await assertFipsException(
'verify',
algorithm,
() => subtle.verify(algorithm, key, data, data),
'Invalid KmacParams outputLength');
}
+25 -18
View File
@@ -5,7 +5,7 @@ const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const { hasOpenSSL } = require('../common/crypto');
const { hasFIPS, hasOpenSSL } = require('../common/crypto');
if (!hasOpenSSL(3))
common.skip('requires OpenSSL >= 3');
@@ -13,38 +13,45 @@ if (!hasOpenSSL(3))
const assert = require('assert');
const { types: { isCryptoKey } } = require('util');
const { subtle } = globalThis.crypto;
const fips = hasFIPS();
const usages = ['sign', 'verify'];
async function test(name, length) {
length ??= name === 'KMAC128' ? 128 : 256;
const key = await subtle.generateKey({
name,
length,
}, true, usages);
const expectedLength = length ?? (name === 'KMAC128' ? 128 : 256);
const algorithm = { name };
if (length !== undefined)
algorithm.length = length;
assert(key);
assert(isCryptoKey(key));
if (fips && length !== undefined &&
(length < 32 || length % 8 !== 0)) return;
assert.strictEqual(key.type, 'secret');
assert.strictEqual(key.toString(), '[object CryptoKey]');
assert.strictEqual(key.extractable, true);
assert.deepStrictEqual(key.usages, usages);
assert.strictEqual(key.algorithm.name, name);
assert.strictEqual(key.algorithm.length, length);
assert.strictEqual(key.algorithm, key.algorithm);
assert.strictEqual(key.usages, key.usages);
const generatedKey = await subtle.generateKey(algorithm, true, usages);
const raw = await subtle.exportKey('raw-secret', key);
assert.strictEqual(raw.byteLength, Math.ceil(length / 8));
assert(generatedKey);
assert(isCryptoKey(generatedKey));
assert.strictEqual(generatedKey.type, 'secret');
assert.strictEqual(generatedKey.toString(), '[object CryptoKey]');
assert.strictEqual(generatedKey.extractable, true);
assert.deepStrictEqual(generatedKey.usages, usages);
assert.strictEqual(generatedKey.algorithm.name, name);
assert.strictEqual(generatedKey.algorithm.length, expectedLength);
assert.strictEqual(generatedKey.algorithm, generatedKey.algorithm);
assert.strictEqual(generatedKey.usages, generatedKey.usages);
const raw = await subtle.exportKey('raw-secret', generatedKey);
assert.strictEqual(raw.byteLength, Math.ceil(expectedLength / 8));
}
const kTests = [
['KMAC128', 0],
['KMAC128', 32],
['KMAC128', 128],
['KMAC128', 256],
['KMAC128'],
['KMAC256', 0],
['KMAC256', 32],
['KMAC256', 128],
['KMAC256', 256],
['KMAC256'],
@@ -142,16 +142,24 @@ if (supports('digest', 'cSHAKE128')) {
outputLength: 256,
customization: new Uint8Array([1, 2, 3]),
};
const expected = new Uint8Array(await subtle.digest(algorithm, data));
const plain = new Uint8Array(
await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data));
assert.notDeepStrictEqual(expected, plain);
await withPoisoned(poisonTypedArrayByteLength(0),
common.mustCall(async () => {
assert.deepStrictEqual(
new Uint8Array(await subtle.digest(algorithm, data)),
expected);
}));
if (getFips() === 1) {
await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() =>
assert.rejects(subtle.digest(algorithm, data), {
name: 'NotSupportedError',
message: 'Unsupported CShakeParams customization',
})));
} else {
const expected = new Uint8Array(await subtle.digest(algorithm, data));
const plain = new Uint8Array(
await subtle.digest({ name: 'cSHAKE128', outputLength: 256 }, data));
assert.notDeepStrictEqual(expected, plain);
await withPoisoned(poisonTypedArrayByteLength(0),
common.mustCall(async () => {
assert.deepStrictEqual(
new Uint8Array(await subtle.digest(algorithm, data)),
expected);
}));
}
}
}
@@ -291,17 +299,17 @@ await withPoisoned(
// enforceRangeOptions(): [EnforceRange] uses IntegerPart, not round-half-even.
{
const key = await subtle.importKey(
'raw-secret', new Uint8Array(4), 'PBKDF2', false, ['deriveBits']);
'raw-secret', new Uint8Array(32), 'PBKDF2', false, ['deriveBits']);
const pbkdf2 = (iterations) => subtle.deriveBits({
name: 'PBKDF2',
hash: 'SHA-256',
salt: new Uint8Array(16),
iterations,
}, key, 8);
}, key, 112);
const expected = new Uint8Array(await pbkdf2(1));
const expected = new Uint8Array(await pbkdf2(1000));
await withPoisoned(inherited('clamp', true), common.mustCall(async () => {
assert.deepStrictEqual(new Uint8Array(await pbkdf2(1.5)), expected);
assert.deepStrictEqual(new Uint8Array(await pbkdf2(1000.5)), expected);
}));
}
@@ -12,15 +12,24 @@ if (!hasOpenSSL(3))
const assert = require('assert');
const { subtle } = globalThis.crypto;
const fips = hasFIPS();
const fips4 = hasFIPS(4);
const vectors = require('../fixtures/crypto/kmac')();
function isFipsUnsupported(err) {
function isFipsProviderUnsupported(err) {
return err.name === 'OperationError' &&
err.cause?.code === 'ERR_OSSL_EVP_UNSUPPORTED';
}
function usesNonFipsImplementation({ key, keyLength, outputLength }) {
const keyLengthInBits = keyLength ?? key.byteLength * 8;
return outputLength === 0 ||
outputLength % 8 !== 0 ||
keyLengthInBits < 32 ||
keyLengthInBits % 8 !== 0;
}
function isFips4Incompatible({ key, keyLength, outputLength }) {
const keyLengthInBits = keyLength ?? key.byteLength * 8;
return keyLengthInBits < 128 ||
@@ -206,9 +215,13 @@ async function testSign({ algorithm,
const variations = [];
for (const vector of vectors) {
if (fips && usesNonFipsImplementation(vector)) continue;
if (fips4 && isFips4Incompatible(vector)) {
variations.push(assert.rejects(testVerify(vector), isFipsUnsupported));
variations.push(assert.rejects(testSign(vector), isFipsUnsupported));
variations.push(assert.rejects(
testVerify(vector), isFipsProviderUnsupported));
variations.push(assert.rejects(
testSign(vector), isFipsProviderUnsupported));
} else {
variations.push(testVerify(vector));
variations.push(testSign(vector));
@@ -227,24 +240,18 @@ async function testSign({ algorithm,
['sign', 'verify']);
const algorithm = {
name: 'KMAC128',
outputLength: fips4 ? 16 : 9,
outputLength: fips ? 16 : 9,
customization: new Uint8Array(),
};
const data = new Uint8Array([1, 2, 3]);
if (fips4) {
await assert.rejects(
subtle.sign({ ...algorithm, outputLength: 9 }, key, data),
isFipsUnsupported);
}
const signature = await subtle.sign(algorithm, key, data);
assert.strictEqual(signature.byteLength, 2);
if (!fips4)
if (!fips)
assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0);
assert(await subtle.verify(algorithm, key, signature, data));
if (fips4) {
if (fips) {
const signature128 = await subtle.sign({
...algorithm,
outputLength: 128,
@@ -264,25 +271,23 @@ async function testSign({ algorithm,
}
const invalidSignature = new Uint8Array(signature);
if (fips4)
if (fips)
invalidSignature[0] ^= 0b00000001;
else
invalidSignature[1] |= 0b00000001;
assert(!(await subtle.verify(algorithm, key, invalidSignature, data)));
const nonByteKey = await subtle.importKey(
'raw-secret',
new Uint8Array([0xff, 0xff, 0xff, 0xff]),
{ name: 'KMAC128', length: 25 },
false,
['sign', 'verify']);
const nonByteKeySignature = subtle.sign({
...algorithm,
outputLength: 16,
}, nonByteKey, data);
if (fips4) {
await assert.rejects(nonByteKeySignature, isFipsUnsupported);
} else {
if (!fips) {
const nonByteKey = await subtle.importKey(
'raw-secret',
new Uint8Array([0xff, 0xff, 0xff, 0xff]),
{ name: 'KMAC128', length: 25 },
false,
['sign', 'verify']);
const nonByteKeySignature = subtle.sign({
...algorithm,
outputLength: 16,
}, nonByteKey, data);
const result = await nonByteKeySignature;
assert.strictEqual(result.byteLength, 2);
assert(await subtle.verify({
@@ -293,6 +298,8 @@ async function testSign({ algorithm,
})().then(common.mustCall());
(async function() {
if (fips) return;
const data = new Uint8Array([1, 2, 3]);
for (const name of ['KMAC128', 'KMAC256']) {
@@ -311,13 +318,9 @@ async function testSign({ algorithm,
const algorithm = { name, outputLength: 256 };
const signature = subtle.sign(algorithm, key, data);
if (fips4) {
await assert.rejects(signature, isFipsUnsupported);
} else {
const result = await signature;
assert.strictEqual(result.byteLength, 32);
assert(await subtle.verify(algorithm, key, result, data));
}
const result = await signature;
assert.strictEqual(result.byteLength, 32);
assert(await subtle.verify(algorithm, key, result, data));
}
}
})().then(common.mustCall());
+1 -1
View File
@@ -485,7 +485,7 @@ async function testNonByteLengthWrapUnwrap({
implicitAlgorithm: hmacAlgorithm,
});
if (hasOpenSSL(3)) {
if (hasOpenSSL(3) && getFips() !== 1) {
const kmacAlgorithm = { name: 'KMAC128' };
const kmacKey = await subtle.importKey(
'raw-secret',
+10 -2
View File
@@ -117,6 +117,12 @@ if (hasFIPS(3)) {
]);
}
if (hasFIPS()) {
skip(
'digest/kangarootwelve.tentative.https.any.js',
'digest/turboshake.tentative.https.any.js');
}
// OpenSSL 3.0 through 3.3 reject SHA-1 signature generation in FIPS mode.
// OpenSSL 3.4 permits it for legacy use cases while marking the operation as
// non-approved through a per-operation FIPS indicator. Node does not expose
@@ -171,8 +177,10 @@ if (hasFIPS(4)) {
]);
}
skipSubtests(
['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]);
if (!hasFIPS()) {
skipSubtests(
['digest/kangarootwelve.tentative.https.any.js', /C=(?:\d{4,}|5(?:1[3-9]|[2-9]\d)|[6-9]\d{2}) bytes/]);
}
function assertNoOverlap(fileSkips, subtestSkips) {
const subtestSkipFiles = new Set(Object.keys(subtestSkips));