mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
7d712ae426
commit
ed9f464b31
20 files changed
+470
-181
No files matched your search
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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());
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user