From de47538f66acac25d9ff664fd20f517c60234846 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 22 Sep 2026 22:23:31 +0200 Subject: [PATCH 1/2] crypto: separate conversion from validation Convert algorithm dictionaries on the original receiver, with name read once. Validate normalized parameters in their operation steps after the method-level key checks and generation usage checks. This also makes Argon2 validation use converted parallelism and keeps later dictionary conversion errors ahead of semantic parameter errors. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Reviewed-By: James M Snell Reviewed-By: Aviv Keller --- lib/internal/crypto/aes.js | 7 +- lib/internal/crypto/argon2.js | 2 + lib/internal/crypto/cfrg.js | 3 + lib/internal/crypto/chacha20_poly1305.js | 2 + lib/internal/crypto/diffiehellman.js | 2 + lib/internal/crypto/ec.js | 3 + lib/internal/crypto/hash.js | 2 + lib/internal/crypto/hkdf.js | 2 + lib/internal/crypto/mac.js | 15 ++-- lib/internal/crypto/ml_dsa.js | 3 + lib/internal/crypto/rsa.js | 2 + lib/internal/crypto/util.js | 15 +++- lib/internal/crypto/webcrypto.js | 10 ++- lib/internal/crypto/webidl.js | 89 ++++++++++++++----- .../test-webcrypto-crypto-job-mode.js | 12 +-- test/parallel/test-webcrypto-fips-refresh.js | 37 ++++++++ .../test-webcrypto-normalization-boundary.js | 79 ++++++++++++++++ test/parallel/test-webcrypto-supports-fips.js | 10 ++- test/parallel/test-webcrypto-webidl.js | 10 ++- 19 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 test/parallel/test-webcrypto-fips-refresh.js create mode 100644 test/parallel/test-webcrypto-normalization-boundary.js diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 3280ddff8459..dd95748dcab4 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, getBufferSourceByteLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -173,6 +174,7 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { } function aesCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); @@ -185,8 +187,9 @@ function aesCipher(mode, key, data, algorithm) { function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; - const usagesSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages[name], name)); + const usagesSet = validateKeyUsages(usages, kUsages[name], name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usagesSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index bf9eaedd85a6..844f9dc94a3e 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -29,6 +29,7 @@ const { const { getArrayBufferOrView, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -203,6 +204,7 @@ function validateArgon2DeriveBitsLength(length) { function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); + validateAlgorithm(algorithm, 'deriveBits'); const type = { '__proto__': null, diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 3152362caa8b..2e4bf9a82755 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -23,6 +23,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -184,6 +185,8 @@ function eddsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 45071cded217..32bbcfbf0b74 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -9,6 +9,7 @@ const { const { getUsagesMask, jobPromise, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -36,6 +37,7 @@ function validateKeyLength(length) { } function c20pCipher(mode, key, data, algorithm) { + validateAlgorithm(algorithm, 'encrypt'); return jobPromise(() => new ChaCha20Poly1305CipherJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index ff23246e7199..79264a4c79ab 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -67,6 +67,7 @@ const { numBitsToBytes, toBuf, truncateToBitLength, + validateAlgorithm, kHandle, } = require('internal/crypto/util'); @@ -372,6 +373,7 @@ function diffieHellman(options, callback) { // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. function ecdhDeriveBits(algorithm, baseKey, length) { + validateAlgorithm(algorithm, 'deriveBits'); const { 'public': key } = algorithm; if (getCryptoKeyType(baseKey) !== 'private') { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 7f277addd872..bb857be74c84 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -31,6 +31,7 @@ const { getUsagesMask, jobPromise, normalizeHashName, + validateAlgorithm, kNamedCurveAliases, toUsagesSet, } = require('internal/crypto/util'); @@ -72,6 +73,7 @@ function ecGenerateKey(algorithm, extractable, usages) { const { name, namedCurve } = algorithm; const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const keyAlgorithm = { name, namedCurve }; const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); @@ -139,6 +141,7 @@ function ecImportKey( usages, ) { const { name, namedCurve } = algorithm; + validateAlgorithm(algorithm, 'importKey'); let handle; const allowedUsages = kUsages[name]; diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 3c727543248a..143fc24c9408 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -28,6 +28,7 @@ const { normalizeHashName, numBitsToBytes, truncateToBitLength, + validateAlgorithm, validateMaxBufferLength, kHandle, getCachedHashId, @@ -257,6 +258,7 @@ Hmac.prototype._transform = Hash.prototype._transform; function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); + validateAlgorithm(algorithm, 'digest'); switch (algorithm.name) { case 'SHA-1': diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 8673f9b46817..bf8a296bc4ba 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -29,6 +29,7 @@ const { normalizeHashName, toBuf, validateByteSource, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -182,6 +183,7 @@ function validateHkdfDeriveBitsLength(length, hash) { function hkdfDeriveBits(algorithm, baseKey, length) { const { hash, salt, info } = algorithm; validateHkdfDeriveBitsLength(length, hash); + validateAlgorithm(algorithm, 'deriveBits'); if (length === 0) return PromiseResolve(new ArrayBuffer(0)); diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 3297297abd59..742e1ebe5908 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -21,6 +21,7 @@ const { numBitsToBytes, truncateToBitLength, validateKmacKeyLength, + validateAlgorithm, } = require('internal/crypto/util'); const { @@ -71,11 +72,12 @@ function hmacGenerateKey(algorithm, extractable, usages) { const { hash, name, - length = getBlockSize(hash.name), } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + const { length = getBlockSize(hash.name) } = algorithm; + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -95,8 +97,9 @@ function kmacGenerateKey(algorithm, extractable, usages) { }[name], } = algorithm; - const usageSet = validateUsagesNotEmpty( - validateKeyUsages(usages, kUsages, name)); + const usageSet = validateKeyUsages(usages, kUsages, name); + validateAlgorithm(algorithm, 'generateKey'); + validateUsagesNotEmpty(usageSet); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -114,6 +117,7 @@ function macImportKey( usages, ) { const isHmac = algorithm.name === 'HMAC'; + validateAlgorithm(algorithm, 'importKey'); const usagesSet = validateKeyUsages( usages, kUsages, algorithm.name); let handle; @@ -181,6 +185,7 @@ function hmacSignVerify(key, data, algorithm, signature) { } function kmacSignVerify(key, data, algorithm, signature) { + validateAlgorithm(algorithm, 'sign'); const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( kCryptoJobWebCrypto, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 71238c4726f0..581e9beb5d0f 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -25,6 +25,7 @@ const { getUsagesMask, jobPromise, toUsagesSet, + validateAlgorithm, getBufferSourceByteLength, } = require('internal/crypto/util'); @@ -197,6 +198,8 @@ function mlDsaSignVerify(key, data, algorithm, signature) { if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + validateAlgorithm(algorithm, mode === kSignJobModeSign ? 'sign' : 'verify'); + return jobPromise(() => new SignJob( kCryptoJobWebCrypto, mode, diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d8e589f9e03e..8fa3ecdff1b8 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -34,6 +34,7 @@ const { jobPromise, jobPromiseThen, normalizeHashName, + validateAlgorithm, validateMaxBufferLength, toUsagesSet, } = require('internal/crypto/util'); @@ -113,6 +114,7 @@ function rsaKeyGenerate( const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + validateAlgorithm(algorithm, 'generateKey'); const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); let firstNonzeroByte = 0; while (publicExponent[firstNonzeroByte] === 0) diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index d17302715a6e..325ef49cd90e 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -812,8 +812,8 @@ function normalizeAlgorithm(algorithm, op) { return { name: algName }; // 6. - const normalizedAlgorithm = webidl.converters[desiredType]( - { __proto__: algorithm, name: algName }, + const normalizedAlgorithm = webidl.algorithmConverters[desiredType]( + algorithm, kNormalizeAlgorithmOpts, ); // 7. @@ -849,6 +849,16 @@ function normalizeAlgorithm(algorithm, op) { return normalizedAlgorithm; } +function validateAlgorithm(algorithm, op) { + // HMAC's get key length operation handles zero with a TypeError, while + // HmacImportParams otherwise uses the import operation's DataError. + if (op === 'get key length' && algorithm.name === 'HMAC') + return; + webidl ??= require('internal/crypto/webidl'); + const desiredType = getSupportedAlgorithms().algorithms[op]?.[algorithm.name]; + webidl.validators[desiredType]?.(algorithm); +} + function getDataViewOrTypedArrayBuffer(V) { return isDataView(V) ? DataViewPrototypeGetBuffer(V) : TypedArrayPrototypeGetBuffer(V); @@ -1274,6 +1284,7 @@ module.exports = { hasAnyNotIn, validateByteSource, validateKeyOps, + validateAlgorithm, jobPromise, jobPromiseThen, cleanupWebCryptoResult, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 47a01467640a..6585baa6fc51 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -40,6 +40,7 @@ const { codes: { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, }, } = require('internal/errors'); @@ -69,6 +70,7 @@ const { normalizeHashName, numBitsToBytes, prepareWebCryptoResult, + validateAlgorithm, validateMaxBufferLength, getOptionalByteLength, } = require('internal/crypto/util'); @@ -318,7 +320,9 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { } } -function getKeyLength({ name, length, hash }) { +function getKeyLength(algorithm) { + validateAlgorithm(algorithm, 'get key length'); + const { name, length, hash } = algorithm; switch (name) { case 'AES-CTR': case 'AES-CBC': @@ -330,6 +334,8 @@ function getKeyLength({ name, length, hash }) { return length; case 'HMAC': + if (length === 0) + throw new ERR_INVALID_ARG_VALUE('algorithm.length', length, 'must not be zero'); if (length === undefined) { return getBlockSize(hash?.name); } @@ -1874,6 +1880,7 @@ class SubtleCrypto { normalizeAlgorithm(algorithm, 'get shared key length'); sharedKeyLength = getSharedKeyLength(normalizedAlgorithm); normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey'); + validateAlgorithm(normalizedAdditionalAlgorithm, 'importKey'); } catch { return false; } @@ -1902,6 +1909,7 @@ function check(op, alg, length) { let normalizedAlgorithm; try { normalizedAlgorithm = normalizeAlgorithm(alg, op); + validateAlgorithm(normalizedAlgorithm, op); } catch { if (op === 'wrapKey') { return check('encrypt', alg); diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 913a9cc157c5..f8bc4818f75f 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -2,9 +2,12 @@ const { ArrayPrototypeIncludes, + ArrayPrototypePush, + ArrayPrototypeToSorted, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, @@ -134,6 +137,41 @@ const dictAlgorithm = [ converters.Algorithm = createDictionaryConverter( 'Algorithm', dictAlgorithm); +const validators = { __proto__: null }; +const algorithmConverters = { __proto__: null }; + +// Algorithm.name was converted when selecting the registered dictionary. +// Convert the remaining members on the original object, and keep operation +// validation separate from Web IDL conversion. +function createAlgorithmDictionaryConverter(name, dictionaries) { + const members = []; + const fullMembers = [dictAlgorithm]; + const checks = []; + for (let i = 1; i < dictionaries.length; i++) { + const dictionary = []; + const sorted = ArrayPrototypeToSorted(dictionaries[i], (a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)); + for (let j = 0; j < sorted.length; j++) { + const member = sorted[j]; + if (ObjectPrototypeHasOwnProperty(member, 'validator')) { + ArrayPrototypePush(checks, { key: member.key, validator: member.validator }); + } + ArrayPrototypePush(dictionary, { ...member, validator: undefined }); + } + ArrayPrototypePush(members, dictionary); + ArrayPrototypePush(fullMembers, dictionary); + } + validators[name] = (algorithm) => { + for (let i = 0; i < checks.length; i++) { + const check = checks[i]; + const value = algorithm[check.key]; + if (value !== undefined) + check.validator(value, algorithm); + } + }; + algorithmConverters[name] = createDictionaryConverter(name, members); + return createDictionaryConverter(name, fullMembers); +} + // TODO(panva): Reject resizable backing stores in a semver-major with: // converters.BigInteger = webidl.Uint8Array; converters.BigInteger = (V, opts = kEmptyObject) => { @@ -196,13 +234,13 @@ const dictRsaKeyGenParams = [ }, ]; -converters.RsaKeyGenParams = createDictionaryConverter( +converters.RsaKeyGenParams = createAlgorithmDictionaryConverter( 'RsaKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, ]); -converters.RsaHashedKeyGenParams = createDictionaryConverter( +converters.RsaHashedKeyGenParams = createAlgorithmDictionaryConverter( 'RsaHashedKeyGenParams', [ dictAlgorithm, dictRsaKeyGenParams, @@ -215,7 +253,7 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter( ], ]); -converters.RsaHashedImportParams = createDictionaryConverter( +converters.RsaHashedImportParams = createAlgorithmDictionaryConverter( 'RsaHashedImportParams', [ dictAlgorithm, [ @@ -229,7 +267,7 @@ converters.RsaHashedImportParams = createDictionaryConverter( converters.NamedCurve = converters.DOMString; -converters.EcKeyImportParams = createDictionaryConverter( +converters.EcKeyImportParams = createAlgorithmDictionaryConverter( 'EcKeyImportParams', [ dictAlgorithm, [ @@ -242,7 +280,7 @@ converters.EcKeyImportParams = createDictionaryConverter( ], ]); -converters.EcKeyGenParams = createDictionaryConverter( +converters.EcKeyGenParams = createAlgorithmDictionaryConverter( 'EcKeyGenParams', [ dictAlgorithm, [ @@ -255,7 +293,7 @@ converters.EcKeyGenParams = createDictionaryConverter( ], ]); -converters.AesKeyGenParams = createDictionaryConverter( +converters.AesKeyGenParams = createAlgorithmDictionaryConverter( 'AesKeyGenParams', [ dictAlgorithm, [ @@ -319,7 +357,7 @@ function validateCShakeCustomization(V) { validateMaxBufferLength(V, 'CShakeParams.customization', 512); } -converters.RsaPssParams = createDictionaryConverter( +converters.RsaPssParams = createAlgorithmDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, [ @@ -332,7 +370,7 @@ converters.RsaPssParams = createDictionaryConverter( ], ]); -converters.RsaOaepParams = createDictionaryConverter( +converters.RsaOaepParams = createAlgorithmDictionaryConverter( 'RsaOaepParams', [ dictAlgorithm, [ @@ -343,7 +381,7 @@ converters.RsaOaepParams = createDictionaryConverter( ], ]); -converters.EcdsaParams = createDictionaryConverter( +converters.EcdsaParams = createAlgorithmDictionaryConverter( 'EcdsaParams', [ dictAlgorithm, [ @@ -368,7 +406,7 @@ const kHmacDictionaries = [ ]; for (let i = 0; i < kHmacDictionaries.length; i++) { const { 0: name, 1: zeroError } = kHmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -431,7 +469,7 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('priv'), ]); -converters.HkdfParams = createDictionaryConverter( +converters.HkdfParams = createAlgorithmDictionaryConverter( 'HkdfParams', [ dictAlgorithm, [ @@ -454,7 +492,7 @@ converters.HkdfParams = createDictionaryConverter( ], ]); -converters.CShakeParams = createDictionaryConverter( +converters.CShakeParams = createAlgorithmDictionaryConverter( 'CShakeParams', [ dictAlgorithm, [ @@ -478,7 +516,7 @@ converters.CShakeParams = createDictionaryConverter( ], ]); -converters.Pbkdf2Params = createDictionaryConverter( +converters.Pbkdf2Params = createAlgorithmDictionaryConverter( 'Pbkdf2Params', [ dictAlgorithm, [ @@ -501,7 +539,7 @@ converters.Pbkdf2Params = createDictionaryConverter( ], ]); -converters.AesDerivedKeyParams = createDictionaryConverter( +converters.AesDerivedKeyParams = createAlgorithmDictionaryConverter( 'AesDerivedKeyParams', [ dictAlgorithm, [ @@ -515,7 +553,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter( ], ]); -converters.AesCbcParams = createDictionaryConverter( +converters.AesCbcParams = createAlgorithmDictionaryConverter( 'AesCbcParams', [ dictAlgorithm, [ @@ -528,7 +566,7 @@ converters.AesCbcParams = createDictionaryConverter( ], ]); -converters.AeadParams = createDictionaryConverter( +converters.AeadParams = createAlgorithmDictionaryConverter( 'AeadParams', [ dictAlgorithm, [ @@ -592,7 +630,7 @@ converters.AeadParams = createDictionaryConverter( ], ]); -converters.AesCtrParams = createDictionaryConverter( +converters.AesCtrParams = createAlgorithmDictionaryConverter( 'AesCtrParams', [ dictAlgorithm, [ @@ -620,7 +658,7 @@ converters.AesCtrParams = createDictionaryConverter( converters.CryptoKey = createInterfaceConverter( 'CryptoKey', isCryptoKey); -converters.EcdhKeyDeriveParams = createDictionaryConverter( +converters.EcdhKeyDeriveParams = createAlgorithmDictionaryConverter( 'EcdhKeyDeriveParams', [ dictAlgorithm, [ @@ -642,7 +680,7 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter( ], ]); -converters.ContextParams = createDictionaryConverter( +converters.ContextParams = createAlgorithmDictionaryConverter( 'ContextParams', [ dictAlgorithm, [ @@ -676,7 +714,7 @@ converters.ContextParams = createDictionaryConverter( ], ]); -converters.Argon2Params = createDictionaryConverter( +converters.Argon2Params = createAlgorithmDictionaryConverter( 'Argon2Params', [ dictAlgorithm, [ @@ -753,7 +791,7 @@ converters.Argon2Params = createDictionaryConverter( const kKmacDictionaries = ['KmacKeyGenParams', 'KmacImportParams']; for (let i = 0; i < kKmacDictionaries.length; i++) { const name = kKmacDictionaries[i]; - converters[name] = createDictionaryConverter( + converters[name] = createAlgorithmDictionaryConverter( name, [ dictAlgorithm, [ @@ -767,7 +805,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) { ]); } -converters.KmacParams = createDictionaryConverter( +converters.KmacParams = createAlgorithmDictionaryConverter( 'KmacParams', [ dictAlgorithm, [ @@ -790,7 +828,7 @@ converters.KmacParams = createDictionaryConverter( ], ]); -converters.KangarooTwelveParams = createDictionaryConverter( +converters.KangarooTwelveParams = createAlgorithmDictionaryConverter( 'KangarooTwelveParams', [ dictAlgorithm, [ @@ -812,7 +850,7 @@ converters.KangarooTwelveParams = createDictionaryConverter( ], ]); -converters.TurboShakeParams = createDictionaryConverter( +converters.TurboShakeParams = createAlgorithmDictionaryConverter( 'TurboShakeParams', [ dictAlgorithm, [ @@ -842,6 +880,9 @@ converters.TurboShakeParams = createDictionaryConverter( ]); module.exports = { + // Spread into fast-property objects before detaching their prototypes. + algorithmConverters: ObjectSetPrototypeOf({ ...algorithmConverters }, null), converters, requiredArguments, + validators: ObjectSetPrototypeOf({ ...validators }, null), }; diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js index 5f5f1761a4c7..c050ad5c26bf 100644 --- a/test/parallel/test-webcrypto-crypto-job-mode.js +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -15,10 +15,8 @@ const { } = require('internal/crypto/keys'); const { getUsagesMask, + jobPromise, } = require('internal/crypto/util'); -const { - aesCipher, -} = require('internal/crypto/aes'); const { AESCipherJob, @@ -135,11 +133,13 @@ async function withObjectPrototypeSetters(names, fn) { Buffer.alloc(15)), /Invalid initialization vector/); - const promise = aesCipher( + const promise = jobPromise(() => new AESCipherJob( + kCryptoJobWebCrypto, kWebCryptoCipherEncrypt, - key, + getCryptoKeyHandle(key), Buffer.alloc(16), - { name: 'AES-CBC', iv: Buffer.alloc(15) }); + kKeyVariantAES_CBC_128, + Buffer.alloc(15))); assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); await assert.rejects(promise, (err) => { diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js new file mode 100644 index 000000000000..71459099356c --- /dev/null +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -0,0 +1,37 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { getFips, setFips } = require('crypto'); +const { internalBinding } = require('internal/test/binding'); +const { getOptionValue } = require('internal/options'); +if (!internalBinding('crypto').testFipsCrypto()) + common.skip('requires an active FIPS provider'); +if (getOptionValue('--force-fips')) + common.skip('FIPS mode cannot be changed when forced'); + +const initial = getFips(); +try { + for (const fips of [false, true, false]) { + setFips(fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'TurboSHAKE128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'KT128', outputLength: 128, + }), !fips); + assert.strictEqual(SubtleCrypto.supports('digest', { + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), + }), !fips); + assert.strictEqual(SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + }), !fips); + } +} finally { + setFips(initial); +} diff --git a/test/parallel/test-webcrypto-normalization-boundary.js b/test/parallel/test-webcrypto-normalization-boundary.js new file mode 100644 index 000000000000..20102c32e6a5 --- /dev/null +++ b/test/parallel/test-webcrypto-normalization-boundary.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const data = new Uint8Array(16); + const key = await subtle.importKey('raw', data, 'AES-CBC', false, ['encrypt']); + class Parameters { + #iv = new Uint8Array(16); + get name() { return 'AES-CBC'; } + get iv() { return this.#iv; } + } + await subtle.encrypt(new Parameters(), key, data); + + const reads = []; + const algorithm = new Proxy({ name: 'AES-CBC', iv: data }, { + get: common.mustCall((target, member, receiver) => { + assert.strictEqual(receiver, algorithm); + reads.push(member); + return Reflect.get(target, member, receiver); + }, 2), + }); + await subtle.encrypt(algorithm, key, data); + assert.deepStrictEqual(reads, ['name', 'iv']); + + const decryptKey = await subtle.importKey('raw', data, 'AES-CBC', false, ['decrypt']); + await assert.rejects(subtle.encrypt({ name: 'AES-CBC', iv: new Uint8Array(8) }, + decryptKey, data), { name: 'InvalidAccessError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 100 }, false, ['sign']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'AES-CBC', length: 64 }, false, []), + { name: 'OperationError' }); + await assert.rejects(subtle.generateKey({ name: 'HMAC', hash: 'SHA-256', length: 0 }, false, []), + { name: 'OperationError' }); + if (SubtleCrypto.supports('digest', 'SHA3-256')) { + const hmac = { name: 'HMAC', hash: 'SHA3-256' }; + await assert.rejects(subtle.generateKey(hmac, false, ['encrypt']), { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey(hmac, false, []), { name: 'NotSupportedError' }); + } + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, ['encrypt']), + { name: 'SyntaxError' }); + await assert.rejects(subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-999' }, false, []), + { name: 'NotSupportedError' }); + await assert.rejects(subtle.generateKey({ + name: 'RSA-PSS', modulusLength: 0, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', + }, false, []), { name: 'OperationError' }); + await assert.rejects(subtle.encrypt({ name: 'AES-CTR', counter: new Uint8Array(8), length: NaN }, + key, data), TypeError); + assert.strictEqual(SubtleCrypto.supports('encrypt', { name: 'AES-CBC', iv: new Uint8Array(8) }), false); + + const hkdf = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const base = await subtle.importKey('raw', data, 'HKDF', false, ['deriveKey']); + const noDeriveKey = await subtle.importKey('raw', data, 'HKDF', false, ['deriveBits']); + const hmac = { name: 'HMAC', hash: 'SHA-256', length: 0 }; + await assert.rejects(subtle.deriveKey(hkdf, base, hmac, false, ['sign']), TypeError); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, hmac, false, ['sign']), + { name: 'InvalidAccessError' }); + await assert.rejects(subtle.deriveKey(hkdf, noDeriveKey, { name: 'AES-GCM', length: 100 }, false, ['encrypt']), + { name: 'InvalidAccessError' }); + + if (SubtleCrypto.supports('importKey', 'Argon2id')) { + const base = await subtle.importKey('raw-secret', data, 'Argon2id', false, ['deriveBits']); + const parameters = { name: 'Argon2id', nonce: data, passes: 1, memory: 8, parallelism: 1 }; + const expected = await subtle.deriveBits(parameters, base, 256); + let conversions = 0; + const parallelism = { valueOf() { conversions++; return 1.5; } }; + assert.deepStrictEqual(await subtle.deriveBits({ ...parameters, parallelism }, base, 256), expected); + assert.strictEqual(conversions, 1); + assert.strictEqual(SubtleCrypto.supports('deriveBits', { ...parameters, parallelism: 1.5 }, 256), true); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-supports-fips.js b/test/parallel/test-webcrypto-supports-fips.js index ae5d1d4b32d6..e08551c65f35 100644 --- a/test/parallel/test-webcrypto-supports-fips.js +++ b/test/parallel/test-webcrypto-supports-fips.js @@ -50,21 +50,23 @@ async function check() { } } } + const hashes = crypto.getHashes(); + const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; const rsa = { name: 'RSA-PSS', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', }; assert.strictEqual(SubtleCrypto.supports('generateKey', rsa), !fips); if (fips) { - assert.throws(() => normalizeAlgorithm(rsa, 'generateKey'), { + // Hash normalization precedes the RSA operation's modulus validation. + const error = hashes.includes('sha256') ? { name: 'OperationError', message: 'algorithm.modulusLength must be at least 2048', - }); + } : hashError; + await assert.rejects(subtle.generateKey(rsa, true, ['sign']), error); } else { assert.strictEqual(normalizeAlgorithm(rsa, 'generateKey').modulusLength, 1024); } - const hashes = crypto.getHashes(); const salt = new Uint8Array(16); - const hashError = { name: 'NotSupportedError', message: 'Unrecognized algorithm name' }; for (const [name, alias] of [ ['SHA-1', 'sha1'], ['SHA-256', 'sha256'], ['SHA-384', 'sha384'], ['SHA-512', 'sha512'], ['SHA3-256', 'sha3-256'], ['SHA3-384', 'sha3-384'], ['SHA3-512', 'sha3-512'], diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index d91099aed2bf..372d2c0b51b0 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -649,7 +649,9 @@ function assertJsonWebKey(actual, expected) { }); } - assert.throws(() => converters.Argon2Params({ ...good, passes: 0 }, opts), { + const zeroPasses = converters.Argon2Params({ ...good, passes: 0 }, opts); + assert.strictEqual(zeroPasses.passes, 0); + assert.throws(() => webidl.validators.Argon2Params(zeroPasses), { name: 'OperationError', message: 'passes must be > 0', }); @@ -734,11 +736,13 @@ function assertJsonWebKey(actual, expected) { }; assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); - assert.throws(() => converters.Argon2Params({ + const excessiveParallelism = converters.Argon2Params({ ...good, parallelism: maxParallelism + 1, memory: 8 * (maxParallelism + 1), - }, opts), { + }, opts); + assert.strictEqual(excessiveParallelism.parallelism, maxParallelism + 1); + assert.throws(() => webidl.validators.Argon2Params(excessiveParallelism), { name: 'OperationError', message: 'parallelism must be > 0 and <= 16777215', }); From b81beca53b67aaadb5a1032ee41b322f821e0caf Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 23 Sep 2026 13:34:11 +0200 Subject: [PATCH 2/2] crypto: use backend cSHAKE and KMAC Require cSHAKE and KMAC output lengths and KMAC key lengths to be multiples of 8 bits. KMAC keys must be at least 32 bits. Share these restrictions between operations and supports. Use OpenSSL's KMAC provider for all supported inputs and its cSHAKE implementation for non-empty function names or customization strings. Keep using SHAKE when both cSHAKE parameters are empty. Remove the custom Keccak framing, partial-bit handling, and short-key fallback. Document the OpenSSL 4.0 requirement for non-empty cSHAKE parameters and reject customization strings containing null bytes. Keep the documented 512-byte customization limit and let backend failures reach callers. Signed-off-by: Filip Skokan Assisted-by: Codex PR-URL: https://github.com/nodejs/node/pull/66237 Reviewed-By: James M Snell Reviewed-By: Aviv Keller --- doc/api/webcrypto.md | 27 +- lib/internal/crypto/hash.js | 28 +- lib/internal/crypto/mac.js | 1 - lib/internal/crypto/util.js | 2 +- lib/internal/crypto/webcrypto.js | 12 +- lib/internal/crypto/webidl.js | 25 +- src/crypto/crypto_hash.cc | 353 ------------------ src/crypto/crypto_hash.h | 68 ---- src/crypto/crypto_kmac.cc | 97 +---- src/crypto/crypto_kmac.h | 3 +- test/fixtures/crypto/kmac.js | 55 --- .../webcrypto/supports-modern-algorithms.mjs | 48 ++- .../test-crypto-key-objects-to-crypto-key.js | 15 +- .../test-webcrypto-aes-kw-short-input.js | 16 +- test/parallel/test-webcrypto-derivekey.js | 18 +- test/parallel/test-webcrypto-digest.js | 100 +++-- test/parallel/test-webcrypto-export-import.js | 82 ++-- .../test-webcrypto-fips-exceptions.mjs | 72 ++-- test/parallel/test-webcrypto-fips-refresh.js | 6 +- .../test-webcrypto-keccak-byte-alignment.js | 90 +++++ test/parallel/test-webcrypto-keygen-kmac.js | 18 +- .../test-webcrypto-prototype-pollution.mjs | 9 +- .../test-webcrypto-sign-verify-kmac.js | 126 ++----- test/parallel/test-webcrypto-wrap-unwrap.js | 38 +- typings/internalBinding/crypto.d.ts | 13 - 25 files changed, 367 insertions(+), 955 deletions(-) create mode 100644 test/parallel/test-webcrypto-keccak-byte-alignment.js diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 0611f4bb608c..2fc7087cdfad 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1830,6 +1830,9 @@ changes: description: Renamed `cShakeParams.length` to `cShakeParams.outputLength`. --> +When both `functionName` and `customization` are empty or `undefined`, cSHAKE is +equivalent to plain SHAKE. + #### `cShakeParams.name` -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `cShakeParams.functionName` @@ -1859,9 +1863,10 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} The `functionName` member represents the NIST function-name byte string used to -domain-separate functions built on top of cSHAKE. Accepted values are: +domain-separate functions built on top of cSHAKE. Non-empty values require +OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* empty or `undefined` * the ASCII byte sequence `'KMAC'` * the ASCII byte sequence `'TupleHash'` * the ASCII byte sequence `'ParallelHash'` @@ -1878,11 +1883,11 @@ changes: * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `customization` member represents the customization data. Accepted -values are: +The `customization` member represents the customization data. Non-empty values +require OpenSSL 4.0 or later. Accepted values are: -* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE -* up to 512 bytes of arbitrary data +* empty or `undefined` +* up to 512 bytes of data without null bytes ### Class: `EcdhKeyDeriveParams` @@ -2332,7 +2337,7 @@ added: v24.8.0 * Type: {number} The optional number of bits in the KMAC key. This is optional and should -be omitted for most cases. +be omitted for most cases. The key length must be at least 32 and a multiple of 8. #### `kmacImportParams.name` @@ -2382,7 +2387,8 @@ added: v24.8.0 The number of bits to generate for the KMAC key. If omitted, the length will be determined by the KMAC algorithm used. -This is optional and should be omitted for most cases. +This is optional and should be omitted for most cases. Must be at least 32 and a +multiple of 8. #### `kmacKeyGenParams.name` @@ -2416,7 +2422,8 @@ added: v24.8.0 added: v25.9.0 --> -* Type: {number} represents the requested output length in bits. +* Type: {number} represents the requested output length in bits. Must be a + multiple of 8. #### `kmacParams.customization` diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 143fc24c9408..d4dc0b7c4f3c 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -6,12 +6,10 @@ const { StringPrototypeReplace, StringPrototypeToLowerCase, Symbol, - TypedArrayPrototypeGetBuffer, TypedArrayPrototypeIncludes, } = primordials; const { - CShakeJob, Hash: _Hash, HashJob, Hmac: _Hmac, @@ -24,10 +22,7 @@ const { const { getStringOption, jobPromise, - jobPromiseThen, normalizeHashName, - numBitsToBytes, - truncateToBitLength, validateAlgorithm, validateMaxBufferLength, kHandle, @@ -43,7 +38,6 @@ const { } = require('internal/crypto/keys'); const { - lazyDOMException, normalizeEncoding, encodingsMap, getDeprecationWarningEmitter, @@ -284,30 +278,20 @@ function asyncDigest(algorithm, data) { const outputLength = algorithm.outputLength; if (getOptionalByteLength(algorithm.functionName) || getOptionalByteLength(algorithm.customization)) { - if (CShakeJob === undefined) { - throw lazyDOMException( - 'Non-empty CShakeParams functionName or customization is not supported', - 'NotSupportedError'); - } - - return jobPromise(() => new CShakeJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, - algorithm.name, + StringPrototypeToLowerCase(algorithm.name), data, + outputLength, algorithm.functionName, - algorithm.customization, - outputLength)); + algorithm.customization)); } - const bits = jobPromise(() => new HashJob( + return jobPromise(() => new HashJob( kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, - numBitsToBytes(outputLength) * 8)); - if (outputLength % 8 === 0) - return bits; - return jobPromiseThen(bits, (bits) => - TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits))); + outputLength)); } case 'TurboSHAKE128': // Fall through diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 742e1ebe5908..ed2be2b7fdba 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -193,7 +193,6 @@ function kmacSignVerify(key, data, algorithm, signature) { getCryptoKeyHandle(key), algorithm.name, algorithm.customization, - getCryptoKeyAlgorithm(key).length, algorithm.outputLength, data, signature)); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 325ef49cd90e..88aa42b08522 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -721,7 +721,7 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } function validateKmacKeyLength(length) { - if ((length < 32 || length % 8) && isFips()) + if (length < 32 || length % 8 !== 0) throw lazyDOMException('Invalid key length', 'NotSupportedError'); } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 6585baa6fc51..7a2ff063fa1a 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -23,7 +23,6 @@ const { } = primordials; const { - CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -72,7 +71,6 @@ const { prepareWebCryptoResult, validateAlgorithm, validateMaxBufferLength, - getOptionalByteLength, } = require('internal/crypto/util'); const { @@ -1923,15 +1921,7 @@ function check(op, alg, length) { } switch (op) { - case 'digest': { - if ((normalizedAlgorithm.name === 'cSHAKE128' || - normalizedAlgorithm.name === 'cSHAKE256') && - (getOptionalByteLength(normalizedAlgorithm.functionName) || - getOptionalByteLength(normalizedAlgorithm.customization))) { - return CShakeJob !== undefined; - } - return true; - } + case 'digest': case 'decapsulate': case 'decrypt': case 'encapsulate': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index f8bc4818f75f..64fb0a8a226c 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -11,15 +11,13 @@ const { StringPrototypeCharCodeAt, StringPrototypeSplit, StringPrototypeToLowerCase, + TypedArrayPrototypeIncludes, } = primordials; const { lazyDOMException, kEmptyObject, } = require('internal/util'); -const { - isUint32, -} = require('internal/validators'); const { getCryptoKeyAlgorithm, getCryptoKeyType, @@ -30,9 +28,9 @@ const { validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, + getHashes, isFips, kNamedCurveAliases, - numBitsToBytes, validateKmacKeyLength, } = require('internal/crypto/util'); const { @@ -317,20 +315,20 @@ function validateZeroLength(parameterName) { } function validateCShakeOutputLength(V) { - if (!isUint32(numBitsToBytes(V) * 8)) { + if (V % 8 !== 0) { throw lazyDOMException( 'Invalid CShakeParams outputLength', - 'OperationError'); + 'NotSupportedError'); } } const kCShakeFunctionNames = ['KMAC', 'TupleHash', 'ParallelHash']; -function validateCShakeFunctionName(V) { +function validateCShakeFunctionName(V, dict) { const length = getBufferSourceByteLength(V); if (length === 0) return; - if (!isFips()) { + if (ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) { const bytes = getBufferSourceBytes(V); for (let i = 0; i < kCShakeFunctionNames.length; i++) { const functionName = kCShakeFunctionNames[i]; @@ -349,12 +347,17 @@ function validateCShakeFunctionName(V) { 'NotSupportedError'); } -function validateCShakeCustomization(V) { - if (isFips() && getBufferSourceByteLength(V) !== 0) +function validateCShakeCustomization(V, dict) { + if (getBufferSourceByteLength(V) === 0) return; + if (!ArrayPrototypeIncludes(getHashes(), StringPrototypeToLowerCase(dict.name))) throw lazyDOMException( 'Unsupported CShakeParams customization', 'NotSupportedError'); validateMaxBufferLength(V, 'CShakeParams.customization', 512); + if (TypedArrayPrototypeIncludes(getBufferSourceBytes(V), 0)) + throw lazyDOMException( + 'Unsupported CShakeParams customization', + 'NotSupportedError'); } converters.RsaPssParams = createAlgorithmDictionaryConverter( @@ -814,7 +817,7 @@ converters.KmacParams = createAlgorithmDictionaryConverter( converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), validator: (V) => { - if ((V === 0 || V % 8) && isFips()) + if (V % 8 !== 0 || (V === 0 && isFips())) throw lazyDOMException( 'Invalid KmacParams outputLength', 'NotSupportedError'); diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 185c354db711..085cebdef06b 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,21 +7,15 @@ #include "threadpoolwork-inl.h" #include "v8.h" -#if OPENSSL_WITH_EVP_MAC -#include #include -#endif #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #endif #include -#include #include #include -#include -#include #include #include @@ -567,9 +561,6 @@ void Hash::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); HashJob::Initialize(env, target); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::Initialize(env, target); -#endif } void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -581,9 +572,6 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OneShotDigest); HashJob::RegisterExternalReferences(registry); -#if OPENSSL_WITH_EVP_MAC - CShakeJob::RegisterExternalReferences(registry); -#endif } // new Hash(algorithm, xofLen, algorithmId, algorithmCache[, functionName, @@ -875,346 +863,5 @@ bool HashTraits::DeriveBits(Environment* env, return true; } -#if OPENSSL_WITH_EVP_MAC -namespace { - -static constexpr std::array kEmptyString = {}; -static constexpr size_t kKeccakKmac128Rate = 168; -static constexpr size_t kKeccakKmac256Rate = 136; -static constexpr size_t kMaxCShakeCustomizationSize = 512; - -struct EncodedLength { - std::array data; - size_t size; -}; - -struct EncodedStringInput { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct KeccakKmacXof { - ncrypto::EVPMDCtxPointer ctx; - size_t rate; -}; - -size_t EncodedLengthSize(size_t value) { - size_t size = 1; - size_t remaining = value; - while (remaining >>= CHAR_BIT) size++; - return size + 1; -} - -bool AddSize(size_t a, size_t b, size_t* out) { - if (a > std::numeric_limits::max() - b) return false; - *out = a + b; - return true; -} - -EncodedLength EncodeLength(size_t value, bool left) { - const size_t value_size = EncodedLengthSize(value) - 1; - EncodedLength encoded = {{}, value_size + 1}; - - if (left) encoded.data[0] = static_cast(value_size); - for (size_t n = 0; n < value_size; n++) { - const size_t shift = CHAR_BIT * (value_size - n - 1); - encoded.data[(left ? 1 : 0) + n] = - static_cast(value >> shift); - } - if (!left) encoded.data[value_size] = static_cast(value_size); - - return encoded; -} - -bool DigestUpdate(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t size) { - if (size == 0) return true; - return ctx->digestUpdate(ncrypto::Buffer{ - .data = data, - .len = size, - }); -} - -bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) { - static constexpr std::array zeros = {}; - while (size > 0) { - const size_t chunk = std::min(size, zeros.size()); - if (!DigestUpdate(ctx, zeros.data(), chunk)) return false; - size -= chunk; - } - return true; -} - -bool EncodedStringSize(size_t byte_length, size_t bit_length, size_t* size) { - return AddSize(EncodedLengthSize(bit_length), byte_length, size); -} - -bool ByteLengthToBitLength(size_t byte_length, size_t* bit_length) { - if (byte_length > std::numeric_limits::max() / CHAR_BIT) { - return false; - } - *bit_length = byte_length * CHAR_BIT; - return true; -} - -KeccakKmacXof NewKeccakKmacXof(bool use_128_bits) { - // OpenSSL 3.x exposes the cSHAKE/KMAC suffix primitive as KECCAK-KMAC-*. - const char* digest_name = use_128_bits ? OSSL_DIGEST_NAME_KECCAK_KMAC128 - : OSSL_DIGEST_NAME_KECCAK_KMAC256; - auto digest = std::unique_ptr{ - EVP_MD_fetch(nullptr, digest_name, nullptr), EVP_MD_free}; - if (!digest) return {}; - - auto ctx = ncrypto::EVPMDCtxPointer::New(); - if (!ctx.digestInit(digest.get())) return {}; - - return { - .ctx = std::move(ctx), - .rate = use_128_bits ? kKeccakKmac128Rate : kKeccakKmac256Rate, - }; -} - -bool ToEncodedStringInput(const void* data, - size_t byte_length, - EncodedStringInput* input) { - if (byte_length > 0 && data == nullptr) return false; - - size_t bit_length; - if (!ByteLengthToBitLength(byte_length, &bit_length)) return false; - - *input = { - .data = byte_length == 0 ? kEmptyString.data() : data, - .byte_length = byte_length, - .bit_length = bit_length, - }; - return true; -} - -bool DigestUpdateEncodedLength(ncrypto::EVPMDCtxPointer* ctx, - size_t value, - bool left) { - const EncodedLength encoded = EncodeLength(value, left); - return DigestUpdate(ctx, encoded.data.data(), encoded.size); -} - -bool DigestUpdateEncodedString(ncrypto::EVPMDCtxPointer* ctx, - const void* data, - size_t byte_length, - size_t bit_length) { - return DigestUpdateEncodedLength(ctx, bit_length, true) && - DigestUpdate(ctx, data, byte_length); -} - -bool DigestUpdateBytepad(ncrypto::EVPMDCtxPointer* ctx, - size_t width, - const void* data, - size_t byte_length, - size_t bit_length, - const void* data2 = nullptr, - size_t byte_length2 = 0, - size_t bit_length2 = 0) { - if (width == 0) return false; - - size_t encoded_size; - size_t written = EncodedLengthSize(width); - if (!EncodedStringSize(byte_length, bit_length, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - if (data2 != nullptr) { - if (!EncodedStringSize(byte_length2, bit_length2, &encoded_size) || - !AddSize(written, encoded_size, &written)) { - return false; - } - } - - size_t padded_size; - if (!AddSize(written, width - 1, &padded_size)) return false; - padded_size = padded_size / width * width; - DCHECK_GE(padded_size, written); - const size_t padding = padded_size - written; - - return DigestUpdateEncodedLength(ctx, width, true) && - DigestUpdateEncodedString(ctx, data, byte_length, bit_length) && - (data2 == nullptr || - DigestUpdateEncodedString(ctx, data2, byte_length2, bit_length2)) && - DigestUpdateZeros(ctx, padding); -} - -} // namespace - -CShakeConfig::CShakeConfig(CShakeConfig&& other) noexcept - : in(std::move(other.in)), - function_name(std::move(other.function_name)), - customization(std::move(other.customization)), - variant(other.variant), - length(other.length) {} - -CShakeConfig& CShakeConfig::operator=(CShakeConfig&& other) noexcept { - if (&other == this) return *this; - this->~CShakeConfig(); - return *new (this) CShakeConfig(std::move(other)); -} - -void CShakeConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TraitTrackInline(in, "in"); - tracker->TraitTrackInline(function_name, "function_name"); - tracker->TraitTrackInline(customization, "customization"); -} - -MaybeLocal CShakeTraits::EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe CShakeTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params) { - Environment* env = Environment::GetCurrent(args); - - if (IsFipsEnabled()) { - THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env); - return Nothing(); - } - - CHECK(args[offset]->IsString()); // Algorithm name - Utf8Value algorithm_name(env->isolate(), args[offset]); - std::string_view algorithm_str = algorithm_name.ToStringView(); - - if (algorithm_str == "cSHAKE128") { - params->variant = CShakeVariant::CSHAKE128; - } else if (algorithm_str == "cSHAKE256") { - params->variant = CShakeVariant::CSHAKE256; - } else { - UNREACHABLE(); - } - - ArrayBufferOrViewContents data(args[offset + 1]); - if (!data.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "data is too big"); - return Nothing(); - } - params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - - if (!args[offset + 2]->IsUndefined()) { - ArrayBufferOrViewContents function_name(args[offset + 2]); - if (!function_name.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "functionName is too big"); - return Nothing(); - } - params->function_name = IsCryptoJobAsync(mode) - ? function_name.ToCopy() - : function_name.ToByteSource(); - } - - if (!args[offset + 3]->IsUndefined()) { - ArrayBufferOrViewContents customization(args[offset + 3]); - if (!customization.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); - return Nothing(); - } - params->customization = IsCryptoJobAsync(mode) - ? customization.ToCopy() - : customization.ToByteSource(); - } - - CHECK(args[offset + 4]->IsUint32()); // Length - params->length = args[offset + 4].As()->Value(); - - return JustVoid(); -} - -bool CShakeTraits::DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore*) { - CShakeParams cshake_params = { - .variant = params.variant, - .function_name_data = params.function_name.data(), - .function_name_size = params.function_name.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = nullptr, - .input_data = params.in.data(), - .input_size = params.in.size(), - .append_output_length = false, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out) { - if (params.customization_size > kMaxCShakeCustomizationSize) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - auto xof = NewKeccakKmacXof(params.variant == CShakeVariant::CSHAKE128); - if (!xof.ctx) return false; - auto ctx = std::move(xof.ctx); - - EncodedStringInput function_name; - EncodedStringInput customization; - if (!ToEncodedStringInput(params.function_name_data, - params.function_name_size, - &function_name) || - !ToEncodedStringInput(params.customization_data, - params.customization_size, - &customization)) { - return false; - } - - if (!DigestUpdateBytepad(&ctx, - xof.rate, - function_name.data, - function_name.byte_length, - function_name.bit_length, - customization.data, - customization.byte_length, - customization.bit_length)) { - return false; - } - - if (params.bytepad_input != nullptr && - !DigestUpdateBytepad(&ctx, - xof.rate, - params.bytepad_input->data, - params.bytepad_input->byte_length, - params.bytepad_input->bit_length)) { - return false; - } - - if (!DigestUpdate(&ctx, params.input_data, params.input_size)) { - return false; - } - - if (params.append_output_length && - !DigestUpdateEncodedLength(&ctx, params.length, false)) { - return false; - } - - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); - auto data = ctx.digestFinal(length_bytes); - if (!data) [[unlikely]] - return false; - - DCHECK(!data.isSecure()); - *out = ByteSource::Allocated(data.release()); - if (params.length % CHAR_BIT != 0) TruncateToBitLength(params.length, out); - return true; -} -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 146e9cf55b52..1cdfa2054e41 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -86,74 +86,6 @@ struct HashTraits final { using HashJob = DeriveBitsJob; -#if OPENSSL_WITH_EVP_MAC -enum class CShakeVariant { CSHAKE128, CSHAKE256 }; - -struct CShakeBytepadInput final { - const void* data; - size_t byte_length; - size_t bit_length; -}; - -struct CShakeParams final { - CShakeVariant variant; - const void* function_name_data; - size_t function_name_size; - const void* customization_data; - size_t customization_size; - const CShakeBytepadInput* bytepad_input; - const void* input_data; - size_t input_size; - bool append_output_length; - uint32_t length; // Output length in bits -}; - -bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out); - -struct CShakeConfig final : public MemoryRetainer { - ByteSource in; - ByteSource function_name; - ByteSource customization; - CShakeVariant variant; - uint32_t length; // Output length in bits - - CShakeConfig() = default; - - explicit CShakeConfig(CShakeConfig&& other) noexcept; - - CShakeConfig& operator=(CShakeConfig&& other) noexcept; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(CShakeConfig) - SET_SELF_SIZE(CShakeConfig) -}; - -struct CShakeTraits final { - using AdditionalParameters = CShakeConfig; - static constexpr const char* JobName = "CShakeJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_HASHREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - CShakeConfig* params); - - static bool DeriveBits(Environment* env, - const CShakeConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const CShakeConfig& params, - ByteSource* out); -}; - -using CShakeJob = DeriveBitsJob; -#endif // OPENSSL_WITH_EVP_MAC - } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index 4f3efad9f42b..40b39990737f 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -1,14 +1,11 @@ #include "crypto/crypto_kmac.h" #include "async_wrap-inl.h" -#include "crypto/crypto_hash.h" #include "node_internals.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_EVP_MAC #include #include -#include -#include #include #include "crypto/crypto_keys.h" #include "crypto/crypto_sig.h" @@ -26,7 +23,6 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; -using v8::Number; using v8::Object; using v8::Uint32; using v8::Value; @@ -38,7 +34,6 @@ KmacConfig::KmacConfig(KmacConfig&& other) noexcept signature(std::move(other.signature)), customization(std::move(other.customization)), variant(other.variant), - key_length(other.key_length), length(other.length) {} KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { @@ -96,27 +91,18 @@ Maybe KmacTraits::AdditionalConfig( } // If undefined, params->customization remains uninitialized (size 0). - CHECK(args[offset + 4]->IsNumber()); // Key length - double key_length = args[offset + 4].As()->Value(); - if (!(key_length >= 0) || - key_length > static_cast(std::numeric_limits::max())) { - THROW_ERR_OUT_OF_RANGE(env, "key length is too big"); - return Nothing(); - } - params->key_length = static_cast(key_length); + CHECK(args[offset + 4]->IsUint32()); // Length + params->length = args[offset + 4].As()->Value(); - CHECK(args[offset + 5]->IsUint32()); // Length - params->length = args[offset + 5].As()->Value(); - - ArrayBufferOrViewContents data(args[offset + 6]); + ArrayBufferOrViewContents data(args[offset + 5]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (!args[offset + 7]->IsUndefined()) { - ArrayBufferOrViewContents signature(args[offset + 7]); + if (!args[offset + 6]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 6]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -128,86 +114,18 @@ Maybe KmacTraits::AdditionalConfig( return JustVoid(); } -namespace { - -static constexpr std::array kKmacFunctionName = { - 'K', 'M', 'A', 'C'}; -static constexpr size_t kKmacMinOpenSSLKeySize = 4; -// Keep the bit-aware path within OpenSSL's KMAC provider limits. -static constexpr size_t kKmacMaxOpenSSLKeySize = 512; -static constexpr size_t kKmacMaxOpenSSLCustomizationSize = 512; -static constexpr size_t kKmacMaxOpenSSLOutputSize = 0xffffff / CHAR_BIT; - -bool KmacParamsWithinOpenSSLLimits(const KmacConfig& params, - size_t key_size, - size_t length_bytes) { - return key_size <= kKmacMaxOpenSSLKeySize && - NumBitsToBytes(params.key_length) <= kKmacMaxOpenSSLKeySize && - params.customization.size() <= kKmacMaxOpenSSLCustomizationSize && - length_bytes <= kKmacMaxOpenSSLOutputSize; -} - -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; - - CShakeBytepadInput key_input = { - .data = key_data, - .byte_length = key_length_bytes, - .bit_length = params.key_length, - }; - CShakeParams cshake_params = { - .variant = params.variant == KmacVariant::KMAC128 - ? CShakeVariant::CSHAKE128 - : CShakeVariant::CSHAKE256, - .function_name_data = kKmacFunctionName.data(), - .function_name_size = kKmacFunctionName.size(), - .customization_data = params.customization.data(), - .customization_size = params.customization.size(), - .bytepad_input = &key_input, - .input_data = params.data.data(), - .input_size = params.data.size(), - .append_output_length = true, - .length = params.length, - }; - return DeriveCShakeBits(cshake_params, out); -} - -} // namespace - bool KmacTraits::DeriveBits(Environment* env, const KmacConfig& params, ByteSource* out, CryptoJobMode mode, CryptoErrorStore*) { - const bool truncate_to_bit_length = params.length % CHAR_BIT != 0; - const size_t length_bytes = - NumBitsToBytes(static_cast(params.length)); + if (params.length % CHAR_BIT != 0) return false; + const size_t length_bytes = params.length / CHAR_BIT; // Get the key data. const void* key_data = params.key.GetSymmetricKey(); size_t key_size = params.key.GetSymmetricKeySize(); - if (!KmacParamsWithinOpenSSLLimits(params, key_size, length_bytes)) { - return false; - } - - if (params.length == 0) { - *out = ByteSource(); - return true; - } - - // OpenSSL's EVP_MAC provider rejects KMAC keys shorter than 4 bytes. - if (params.length % CHAR_BIT != 0 || params.key_length % CHAR_BIT != 0 || - key_size < kKmacMinOpenSSLKeySize) { - return DeriveBitsWithCShake(params, key_data, key_size, out); - } - // Fetch the KMAC algorithm auto mac = EVPMacPointer::Fetch((params.variant == KmacVariant::KMAC128) ? OSSL_MAC_NAME_KMAC128 @@ -261,7 +179,6 @@ bool KmacTraits::DeriveBits(Environment* env, auto buffer = result.release(); *out = ByteSource::Allocated(buffer.data, buffer.len); - if (truncate_to_bit_length) TruncateToBitLength(params.length, out); return true; } diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 703b03c2c882..4f8fe27d2ff4 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -21,8 +21,7 @@ struct KmacConfig final : public MemoryRetainer { ByteSource signature; ByteSource customization; KmacVariant variant; - size_t key_length; // Key length in bits - uint32_t length; // Output length in bits + uint32_t length; // Output length in bits KmacConfig() = default; diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index ed265bb9c9f3..cc1870af2bb0 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -114,61 +114,6 @@ module.exports = function() { 0x76, 0xfc, 0x89, 0x65, ]), }, - { - // KMAC128 with a short key, generated with OpenSSL's KECCAK-KMAC128 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 256, - expected: Buffer.from([ - 0xfb, 0x7c, 0xbb, 0xa2, 0xa1, 0x0d, 0x1a, 0x87, 0x9a, 0x9f, 0x96, 0x8c, - 0x58, 0x9d, 0x2a, 0xfe, 0x4a, 0x9b, 0xbf, 0x03, 0x7e, 0x85, 0x2c, 0xac, - 0x05, 0xdd, 0x78, 0x5b, 0x78, 0xd6, 0x57, 0x1c, - ]), - }, - { - // KMAC256 with a short key, generated with OpenSSL's KECCAK-KMAC256 - // digest over independently encoded NIST SP 800-185 framing. - algorithm: 'KMAC256', - key: Buffer.from([0x00, 0x01, 0x02]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: Buffer.from('Node.js'), - outputLength: 512, - expected: Buffer.from([ - 0x2c, 0xcf, 0x20, 0xde, 0xd8, 0xc9, 0x6d, 0xb0, 0x5f, 0x15, 0xe0, 0xb3, - 0xce, 0x5d, 0xf0, 0x45, 0xc7, 0xd7, 0x4e, 0xfe, 0x18, 0xee, 0x36, 0xa8, - 0xe4, 0x9a, 0x37, 0xfb, 0xc2, 0xb1, 0xbb, 0xfd, 0xad, 0xf5, 0xb7, 0x89, - 0xd3, 0xa4, 0xbc, 0xb3, 0xa8, 0x28, 0x8e, 0x9f, 0x25, 0xe6, 0x8d, 0x5b, - 0x4a, 0x01, 0x0b, 0x90, 0xae, 0x6d, 0x2b, 0xfc, 0xf1, 0xb6, 0xbb, 0x82, - 0x34, 0x8b, 0x51, 0xd9, - ]), - }, - { - // KMAC128 with a non-byte-aligned output length. The second byte has its - // unused low bits cleared after squeezing a 9-bit result. - algorithm: 'KMAC128', - key: Buffer.from([0x00, 0x01, 0x02, 0x03]), - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 9, - expected: Buffer.from([0x63, 0x80]), - }, - { - // KMAC128 with a non-byte-aligned key length. The raw key is already - // truncated to 25 bits, matching WebCrypto import semantics. - algorithm: 'KMAC128', - key: Buffer.from([0xff, 0xff, 0xff, 0x80]), - keyLength: 25, - data: Buffer.from([0x01, 0x02, 0x03]), - customization: undefined, - outputLength: 128, - expected: Buffer.from([ - 0x25, 0xea, 0xc7, 0x06, 0x82, 0x47, 0x7e, 0x3c, 0x9b, 0xf0, 0xf1, 0x51, - 0x87, 0x46, 0x40, 0x0c, - ]), - }, ]; return vectors; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index b01ebce3e5a5..8d58397783ed 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -11,6 +11,8 @@ const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2) && !fips; const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); +const cshake128 = crypto.getHashes().includes('cshake128'); +const cshake256 = crypto.getHashes().includes('cshake256'); const sha3 = crypto.getHashes().includes('sha3-256'); const ocb = hasOpenSSL(3) && crypto.getCiphers().includes('aes-128-ocb'); const kmac = hasOpenSSL(3) && crypto.getMacs().includes('kmac128'); @@ -22,19 +24,21 @@ export const vectors = { [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], - [shake128 && kmac && !fips, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [cshake128, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], - [shake128, { name: 'cSHAKE128', outputLength: 127 }], + [false, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], - [shake256 && kmac && !fips, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [cshake256, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1, 1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], - [shake256, { name: 'cSHAKE256', outputLength: 255 }], + [false, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], [!fips, { name: 'TurboSHAKE128', outputLength: 128 }], [!fips, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], @@ -86,9 +90,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC128', outputLength: 255 }], + [false, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], - [kmac && !fips, { name: 'KMAC256', outputLength: 255 }], + [false, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -109,10 +113,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -133,10 +141,14 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [kmac && !fips, { name: 'KMAC128', length: 0 }], - [kmac && !fips, { name: 'KMAC256', length: 0 }], - [kmac && !fips, { name: 'KMAC128', length: 1 }], - [kmac && !fips, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC128', length: 0 }], + [false, { name: 'KMAC256', length: 0 }], + [false, { name: 'KMAC128', length: 1 }], + [false, { name: 'KMAC128', length: 24 }], + [kmac, { name: 'KMAC128', length: 32 }], + [false, { name: 'KMAC256', length: 1 }], + [false, { name: 'KMAC256', length: 24 }], + [kmac, { name: 'KMAC256', length: 32 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -251,7 +263,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], @@ -283,7 +295,7 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 249 }], - [pqc && kmac && !fips, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [hybridKems, 'MLKEM768-P256', 'HKDF'], [hybridKems, 'MLKEM768-X25519', 'HKDF'], [hybridKems, 'MLKEM1024-P384', 'HKDF'], diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index eeb5c90c1668..6024029da1b2 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -14,7 +14,6 @@ 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) => { @@ -132,19 +131,11 @@ function genericSecretVectors(name) { ]; } -function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { +function macInvalid(algorithm, invalidLengthMessage, isKmac = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - if (allowZeroKey && !fips) { - const zeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey(algorithm, true, usages); - assert.strictEqual(zeroKey.algorithm.length, 0); - - const explicitZeroKey = createSecretKey(Buffer.alloc(0)) - .toCryptoKey({ ...algorithm, length: 0 }, true, usages); - assert.strictEqual(explicitZeroKey.algorithm.length, 0); - } else if (allowZeroKey) { + if (isKmac) { for (const zeroAlgorithm of [algorithm, { ...algorithm, length: 0 }]) { assert.throws(() => { createSecretKey(Buffer.alloc(0)) @@ -170,7 +161,7 @@ function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { assert.throws( () => key.toCryptoKey({ ...algorithm, length: 0 }, true, usages), - allowZeroKey && fips ? { + isKmac ? { name: 'NotSupportedError', message: 'Invalid key length', } : { diff --git a/test/parallel/test-webcrypto-aes-kw-short-input.js b/test/parallel/test-webcrypto-aes-kw-short-input.js index 09fe0268ab9b..e11e19360de9 100644 --- a/test/parallel/test-webcrypto-aes-kw-short-input.js +++ b/test/parallel/test-webcrypto-aes-kw-short-input.js @@ -6,18 +6,13 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips } = require('crypto'); -const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; (async () => { const keyToWrap = await subtle.importKey( 'raw', new Uint8Array(16), 'AES-GCM', true, ['encrypt']); - let emptyKey; - if (hasOpenSSL(3) && getFips() !== 1) { - emptyKey = await subtle.importKey( - 'raw-secret', new Uint8Array(0), 'KMAC128', true, ['sign']); - } + const shortKey = await subtle.importKey( + 'raw', new Uint8Array(8), { name: 'HMAC', hash: 'SHA-256' }, true, ['sign']); for (const length of [128, 192, 256]) { const wrappingKey = await subtle.generateKey( @@ -31,11 +26,8 @@ const { subtle } = globalThis.crypto; 'HKDF', false, ['deriveBits']), { name: 'OperationError' }); } - if (emptyKey !== undefined) { - await assert.rejects(subtle.wrapKey( - 'raw-secret', emptyKey, wrappingKey, 'AES-KW'), - { name: 'OperationError' }); - } + await assert.rejects(subtle.wrapKey( + 'raw', shortKey, wrappingKey, 'AES-KW'), { name: 'OperationError' }); const wrapped = await subtle.wrapKey( 'raw', keyToWrap, wrappingKey, 'AES-KW'); diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index c567edfbb4e9..675b740d939d 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -283,7 +283,7 @@ const fips4 = hasFIPS(4); })().then(common.mustCall()); } -if (hasOpenSSL(3) && !hasFIPS()) { +if (hasOpenSSL(3)) { (async () => { const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; const usages = ['sign']; @@ -313,19 +313,9 @@ if (hasOpenSSL(3) && !hasFIPS()) { baseKeyAlgorithm, false, ['deriveKey']); - const derived = await subtle.deriveKey( - algorithm, - baseKey, - derivedKeyAlgorithm, - false, - usages); - assert.strictEqual(derived.algorithm.length, 0); - - const signature = subtle.sign({ - name: 'KMAC128', - outputLength: 256, - }, derived, new Uint8Array()); - assert.strictEqual((await signature).byteLength, 32); + await assert.rejects( + subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 47a56a912d69..89bd86bd9284 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,8 +9,7 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); -const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto'); -const fips = hasFIPS(); +const { isBoringSSL } = require('../common/crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -265,16 +264,16 @@ if (getHashes().includes('shake128')) { new Uint8Array(0), ); - const digest = await subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)); - assert.strictEqual(digest.byteLength, 1); - assert.strictEqual(new Uint8Array(digest)[0] & 0b00000001, 0); + await assert.rejects( + subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); await assert.rejects( subtle.digest( { name: 'cSHAKE128', outputLength: 0xffffffff }, Buffer.alloc(1)), { - name: 'OperationError', + name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength', }); @@ -291,22 +290,53 @@ if (getHashes().includes('shake128')) { message: 'Unsupported CShakeParams functionName', }); - if (fips) return; - - await assert.rejects( - subtle.digest( - { - name: 'cSHAKE128', + for (const name of ['cSHAKE128', 'cSHAKE256']) { + const supported = getHashes().includes(name.toLowerCase()); + assert.deepStrictEqual( + await subtle.digest({ + name, outputLength: 256, - customization: Buffer.alloc(513), - }, - Buffer.alloc(1)), - { - name: 'OperationError', - message: 'CShakeParams.customization must be at most 512 bytes', - }); - - if (!hasOpenSSL(3)) return; + functionName: Buffer.alloc(0), + customization: Buffer.alloc(0), + }, Buffer.alloc(1)), + await subtle.digest({ name, outputLength: 256 }, Buffer.alloc(1))); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.alloc(513, 1), + }, Buffer.alloc(1)), + supported ? { + name: 'OperationError', + message: 'CShakeParams.customization must be at most 512 bytes', + } : { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams customization', + }); + + await assert.rejects( + subtle.digest({ + name, + outputLength: 256, + customization: Buffer.from([0x61, 0x00, 0x62]), + }, Buffer.alloc(1)), + { name: 'NotSupportedError', message: 'Unsupported CShakeParams customization' }); + + for (const params of [ + { functionName: Buffer.from('KMAC') }, + { customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, Buffer.alloc(1))).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, Buffer.alloc(1)), { + name: 'NotSupportedError', + }); + } + } + } const nistCShakeShortInput = Buffer.from('00010203', 'hex'); const nistCShakeLongInput = @@ -400,19 +430,23 @@ if (getHashes().includes('shake128')) { 'ca6f88db415829', }, ]) { - assert.strictEqual( - Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), - expected); + if (getHashes().includes(algorithm.name.toLowerCase())) { + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } } - const truncated = Buffer.from(await subtle.digest( - { ...nistCShakeSample1.algorithm, outputLength: 255 }, - 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]); + if (getHashes().includes('cshake128')) { + await assert.rejects( + subtle.digest( + { ...nistCShakeSample1.algorithm, outputLength: 255 }, + nistCShakeSample1.data), + { name: 'NotSupportedError', message: 'Invalid CShakeParams outputLength' }); + } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 385ae8c65e9d..63fdde05f1ef 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -285,69 +285,35 @@ if (hasOpenSSL(3)) { [/* empty usages */]), { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); - { - if (getFips() !== 1) { - 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); + for (const algorithm of [name, { name, length: 0 }]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(), algorithm, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } - 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( - 'raw-secret', - new Uint8Array([0xff]), - { name, length: 0 }, - true, - ['sign', 'verify']), - { name: 'DataError', message: 'Invalid key length' }); - - 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: 'NotSupportedError', message: 'Invalid key length' }); + + await assert.rejects( + subtle.generateKey({ name, length: 9 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); - const importedExplicit = await subtle.importKey( + for (const byteLength of [1, 2]) { + await assert.rejects( + subtle.importKey( 'raw-secret', - new Uint8Array([0xff, 0xff]), + new Uint8Array(byteLength).fill(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' }); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } diff --git a/test/parallel/test-webcrypto-fips-exceptions.mjs b/test/parallel/test-webcrypto-fips-exceptions.mjs index ecc0f3c6989d..b45740dc508e 100644 --- a/test/parallel/test-webcrypto-fips-exceptions.mjs +++ b/test/parallel/test-webcrypto-fips-exceptions.mjs @@ -12,10 +12,10 @@ if (!hasFIPS(3)) common.skip('requires OpenSSL >= 3 in FIPS mode'); const require = createRequire(import.meta.url); +const { getHashes } = require('node:crypto'); const { internalBinding } = require('internal/test/binding'); const { getCryptoKeyHandle } = require('internal/crypto/keys'); const { - CShakeJob, KangarooTwelveJob, KmacJob, TurboShakeJob, @@ -52,13 +52,6 @@ for (const createJob of [ 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', @@ -66,48 +59,34 @@ for (const createJob of [ }); } -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', +for (const name of ['cSHAKE128', 'cSHAKE256']) { + const emptyCShake = { + name, outputLength: 256, - customization: new Uint8Array(length), + customization: data, + functionName: data, }; - await assertFipsException( - 'digest', - algorithm, - () => subtle.digest(algorithm, data), - 'Unsupported CShakeParams customization'); + assert.strictEqual(SubtleCrypto.supports('digest', emptyCShake), true); + assert.strictEqual((await subtle.digest(emptyCShake, data)).byteLength, 32); + + for (const params of [ + { customization: Buffer.from('Node.js') }, + { functionName: Buffer.from('KMAC') }, + { functionName: Buffer.from('KMAC'), customization: Buffer.from('Node.js') }, + ]) { + const algorithm = { name, outputLength: 256, ...params }; + const supported = getHashes().includes(name.toLowerCase()); + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), supported); + if (supported) { + assert.strictEqual((await subtle.digest(algorithm, data)).byteLength, 32); + } else { + await assert.rejects(subtle.digest(algorithm, data), { + name: 'NotSupportedError', + }); + } + } } -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( @@ -170,7 +149,6 @@ await assert.rejects( getCryptoKeyHandle(key), 'KMAC128', undefined, - 32, 9, data, undefined).run(), diff --git a/test/parallel/test-webcrypto-fips-refresh.js b/test/parallel/test-webcrypto-fips-refresh.js index 71459099356c..4eff258b72b9 100644 --- a/test/parallel/test-webcrypto-fips-refresh.js +++ b/test/parallel/test-webcrypto-fips-refresh.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); -const { getFips, setFips } = require('crypto'); +const { getFips, getHashes, setFips } = require('crypto'); const { internalBinding } = require('internal/test/binding'); const { getOptionValue } = require('internal/options'); if (!internalBinding('crypto').testFipsCrypto()) @@ -25,8 +25,8 @@ try { name: 'KT128', outputLength: 128, }), !fips); assert.strictEqual(SubtleCrypto.supports('digest', { - name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array(1), - }), !fips); + name: 'cSHAKE128', outputLength: 128, customization: new Uint8Array([1]), + }), getHashes().includes('cshake128')); assert.strictEqual(SubtleCrypto.supports('generateKey', { name: 'RSA-PSS', hash: 'SHA-256', modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), diff --git a/test/parallel/test-webcrypto-keccak-byte-alignment.js b/test/parallel/test-webcrypto-keccak-byte-alignment.js new file mode 100644 index 000000000000..ce3201210eb6 --- /dev/null +++ b/test/parallel/test-webcrypto-keccak-byte-alignment.js @@ -0,0 +1,90 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createSecretKey, subtle } = require('crypto'); +const { SubtleCrypto } = globalThis; +const unsupported = { name: 'NotSupportedError' }; + +(async () => { + const data = new Uint8Array([1, 2, 3]); + for (const name of ['cSHAKE128', 'cSHAKE256']) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128 })) + continue; + + for (const customization of [undefined, new Uint8Array([1])]) { + if (!SubtleCrypto.supports('digest', { name, outputLength: 128, customization })) + continue; + + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, outputLength: 128 + remainder, customization }; + assert.strictEqual(SubtleCrypto.supports('digest', algorithm), false); + await assert.rejects(subtle.digest(algorithm, data), unsupported); + } + + const digest = await subtle.digest({ name, outputLength: 128, customization }, data); + assert.strictEqual(digest.byteLength, 16); + } + + // Check the uint32 boundary without allocating a large digest. + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength: 0xfffffff8 }), true); + for (const outputLength of [0xfffffff9, 0xffffffff]) { + assert.strictEqual(SubtleCrypto.supports('digest', { name, outputLength }), false); + await assert.rejects(subtle.digest({ name, outputLength }, data), unsupported); + } + } + + const baseKey = await subtle.importKey('raw-secret', data, 'HKDF', false, ['deriveKey']); + const derivation = { name: 'HKDF', hash: 'SHA-256', salt: data, info: data }; + const raw = Buffer.alloc(32, 1); + const secretKey = createSecretKey(raw); + + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const key = await subtle.importKey('raw-secret', raw, name, false, ['sign', 'verify']); + for (let remainder = 1; remainder < 8; remainder++) { + const algorithm = { name, length: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('generateKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('importKey', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('deriveKey', derivation, algorithm), false); + await assert.rejects(subtle.generateKey(algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey('raw-secret', raw, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.importKey( + 'jwk', { kty: 'oct', k: raw.toString('base64url') }, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.deriveKey(derivation, baseKey, algorithm, false, ['sign']), unsupported); + assert.throws(() => secretKey.toCryptoKey(algorithm, false, ['sign']), unsupported); + + const params = { name, outputLength: 248 + remainder }; + assert.strictEqual(SubtleCrypto.supports('sign', params), false); + assert.strictEqual(SubtleCrypto.supports('verify', params), false); + await assert.rejects(subtle.sign(params, key, data), unsupported); + await assert.rejects(subtle.verify(params, key, raw, data), unsupported); + } + } + + if (SubtleCrypto.supports('generateKey', 'ML-KEM-768')) { + const { publicKey, privateKey } = await subtle.generateKey( + 'ML-KEM-768', false, ['encapsulateBits', 'encapsulateKey', 'decapsulateKey']); + const { ciphertext } = await subtle.encapsulateBits('ML-KEM-768', publicKey); + for (const name of ['KMAC128', 'KMAC256']) { + if (!SubtleCrypto.supports('importKey', name)) + continue; + + const algorithm = { name, length: 255 }; + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', algorithm), false); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', algorithm), false); + await assert.rejects(subtle.encapsulateKey( + 'ML-KEM-768', publicKey, algorithm, false, ['sign']), unsupported); + await assert.rejects(subtle.decapsulateKey( + 'ML-KEM-768', privateKey, ciphertext, algorithm, false, ['sign']), unsupported); + + assert.strictEqual(SubtleCrypto.supports('encapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + assert.strictEqual(SubtleCrypto.supports('decapsulateKey', 'ML-KEM-768', { name, length: 256 }), true); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index 33716095751f..999e70df6a60 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -5,7 +5,7 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); @@ -13,7 +13,6 @@ if (!hasOpenSSL(3)) const assert = require('assert'); const { types: { isCryptoKey } } = require('util'); const { subtle } = globalThis.crypto; -const fips = hasFIPS(); const usages = ['sign', 'verify']; @@ -23,9 +22,6 @@ async function test(name, length) { if (length !== undefined) algorithm.length = length; - if (fips && length !== undefined && - (length < 32 || length % 8 !== 0)) return; - const generatedKey = await subtle.generateKey(algorithm, true, usages); assert(generatedKey); @@ -45,12 +41,10 @@ async function test(name, length) { } const kTests = [ - ['KMAC128', 0], ['KMAC128', 32], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], - ['KMAC256', 0], ['KMAC256', 32], ['KMAC256', 128], ['KMAC256', 256], @@ -60,3 +54,13 @@ const kTests = [ const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); + +(async () => { + for (const name of ['KMAC128', 'KMAC256']) { + for (const length of [0, 8, 16, 24]) { + await assert.rejects( + subtle.generateKey({ name, length }, true, usages), + { name: 'NotSupportedError', message: 'Invalid key length' }); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-prototype-pollution.mjs b/test/parallel/test-webcrypto-prototype-pollution.mjs index 5ed77fc6021e..b6f2c07f81a4 100644 --- a/test/parallel/test-webcrypto-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-prototype-pollution.mjs @@ -13,8 +13,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const require = createRequire(import.meta.url); const { kSupportedAlgorithms } = require('internal/crypto/util'); -const { getFips } = require('node:crypto'); -const { hasOpenSSL } = require('../common/crypto'); +const { getFips, getHashes } = require('node:crypto'); const { subtle } = globalThis.crypto; const TypedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); @@ -134,15 +133,15 @@ if (supports('digest', 'cSHAKE128')) { message: /Unsupported CShakeParams functionName/, }))); - // asyncDigest() picks the cSHAKE job over plain SHAKE on a non-empty + // asyncDigest() picks cSHAKE over plain SHAKE on a non-empty // customization. - if (hasOpenSSL(3)) { + { const algorithm = { name: 'cSHAKE128', outputLength: 256, customization: new Uint8Array([1, 2, 3]), }; - if (getFips() === 1) { + if (!getHashes().includes('cshake128')) { await withPoisoned(poisonTypedArrayByteLength(0), common.mustCall(() => assert.rejects(subtle.digest(algorithm, data), { name: 'NotSupportedError', diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index ac0b738bcd57..5829ab3685c0 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -5,38 +5,16 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasFIPS, hasOpenSSL } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); if (!hasOpenSSL(3)) common.skip('requires OpenSSL >= 3'); const assert = require('assert'); const { subtle } = globalThis.crypto; -const fips = hasFIPS(); -const fips4 = hasFIPS(4); const vectors = require('../fixtures/crypto/kmac')(); -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 || - keyLengthInBits % 8 !== 0 || - outputLength % 8 !== 0; -} - async function testVerify({ algorithm, key, keyLength, @@ -215,17 +193,8 @@ 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), isFipsProviderUnsupported)); - variations.push(assert.rejects( - testSign(vector), isFipsProviderUnsupported)); - } else { - variations.push(testVerify(vector)); - variations.push(testSign(vector)); - } + variations.push(testVerify(vector)); + variations.push(testSign(vector)); } await Promise.all(variations); @@ -240,87 +209,54 @@ async function testSign({ algorithm, ['sign', 'verify']); const algorithm = { name: 'KMAC128', - outputLength: fips ? 16 : 9, + outputLength: 16, customization: new Uint8Array(), }; const data = new Uint8Array([1, 2, 3]); const signature = await subtle.sign(algorithm, key, data); assert.strictEqual(signature.byteLength, 2); - if (!fips) - assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); assert(await subtle.verify(algorithm, key, signature, data)); - if (fips) { - const signature128 = await subtle.sign({ - ...algorithm, - outputLength: 128, - }, key, data); - assert.strictEqual(signature128.byteLength, 16); - assert(await subtle.verify({ - ...algorithm, - outputLength: 128, - }, key, signature128, data)); - } else { - const signature16 = new Uint8Array(await subtle.sign({ - ...algorithm, - outputLength: 16, - }, key, data)); - signature16[1] &= 0b10000000; - assert.notDeepStrictEqual(new Uint8Array(signature), signature16); - } + const signature128 = await subtle.sign({ + ...algorithm, + outputLength: 128, + }, key, data); + assert.strictEqual(signature128.byteLength, 16); + assert(await subtle.verify({ + ...algorithm, + outputLength: 128, + }, key, signature128, data)); const invalidSignature = new Uint8Array(signature); - if (fips) - invalidSignature[0] ^= 0b00000001; - else - invalidSignature[1] |= 0b00000001; + invalidSignature[0] ^= 0b00000001; assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); - if (!fips) { - const nonByteKey = await subtle.importKey( + const nonByteOutput = { ...algorithm, outputLength: 9 }; + await assert.rejects( + subtle.sign(nonByteOutput, key, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + await assert.rejects( + subtle.verify(nonByteOutput, key, signature, data), + { name: 'NotSupportedError', message: 'Invalid KmacParams outputLength' }); + + await assert.rejects( + 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({ - ...algorithm, - outputLength: 16, - }, nonByteKey, result, data)); - } + ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); })().then(common.mustCall()); (async function() { - if (fips) return; - - const data = new Uint8Array([1, 2, 3]); - for (const name of ['KMAC128', 'KMAC256']) { - for (const keyData of [ - new Uint8Array(), - new Uint8Array([1]), - new Uint8Array([1, 2, 3]), - ]) { - const key = await subtle.importKey( - 'raw-secret', - keyData, - { name }, - true, - ['sign', 'verify']); - assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); - - const algorithm = { name, outputLength: 256 }; - const signature = subtle.sign(algorithm, key, data); - const result = await signature; - assert.strictEqual(result.byteLength, 32); - assert(await subtle.verify(algorithm, key, result, data)); + for (const byteLength of [0, 1, 2, 3]) { + await assert.rejects( + subtle.importKey( + 'raw-secret', new Uint8Array(byteLength), name, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); } } })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index b899e4f712d1..1f8d45f02c24 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -485,20 +485,30 @@ async function testNonByteLengthWrapUnwrap({ }); if (hasOpenSSL(3) && getFips() !== 1) { - const kmacAlgorithm = { name: 'KMAC128' }; - const kmacKey = await subtle.importKey( - 'raw-secret', - new Uint8Array([0xff, 0xff]), - { ...kmacAlgorithm, length: 9 }, - true, - ['sign', 'verify']); - await testNonByteLengthWrapUnwrap({ - key: kmacKey, - formats: ['raw-secret', 'jwk'], - rawFormat: 'raw-secret', - explicitAlgorithm: { ...kmacAlgorithm, length: 9 }, - implicitAlgorithm: kmacAlgorithm, - }); + for (const name of ['KMAC128', 'KMAC256']) { + const keyData = new Uint8Array(32).fill(0xff); + const kmacKey = await subtle.importKey( + 'raw-secret', keyData, name, true, ['sign', 'verify']); + const wrappingKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, true, ['wrapKey', 'unwrapKey']); + + for (const [i, format] of ['raw-secret', 'jwk'].entries()) { + const wrapAlgorithm = { name: 'AES-GCM', iv: new Uint8Array(12).fill(i) }; + const wrapped = await subtle.wrapKey(format, kmacKey, wrappingKey, wrapAlgorithm); + await assert.rejects( + subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 255 }, true, ['sign', 'verify']), + { name: 'NotSupportedError', message: 'Invalid key length' }); + + const unwrapped = await subtle.unwrapKey( + format, wrapped, wrappingKey, wrapAlgorithm, + { name, length: 256 }, true, ['sign', 'verify']); + assert.strictEqual(unwrapped.algorithm.length, 256); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey('raw-secret', unwrapped)), keyData); + } + } } })().then(common.mustCall()); diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts index af6455f886fa..e12016ec9161 100644 --- a/typings/internalBinding/crypto.d.ts +++ b/typings/internalBinding/crypto.d.ts @@ -182,17 +182,6 @@ declare namespace InternalCryptoBinding { ): CryptoJobWebCrypto; } - interface CShakeJobConstructor { - new( - mode: CryptoJobWebCryptoMode, - algorithm: string, - data: ByteSource, - functionName: OptionalByteSource, - customization: OptionalByteSource, - outputLength: number, - ): CryptoJobWebCrypto; - } - interface ChaCha20Poly1305CipherJobConstructor { new( mode: CryptoJobWebCryptoMode, @@ -375,7 +364,6 @@ declare namespace InternalCryptoBinding { key: KeyObjectHandle, algorithm: string, customization: OptionalByteSource, - keyLength: number, outputLength: number, data: ByteSource, ...signature: MacJobSignatureArgs @@ -818,7 +806,6 @@ declare namespace InternalCryptoBinding { export interface CryptoBinding { AESCipherJob: InternalCryptoBinding.AESCipherJobConstructor; Argon2Job: InternalCryptoBinding.Argon2JobConstructor; - CShakeJob?: InternalCryptoBinding.CShakeJobConstructor; ChaCha20Poly1305CipherJob: InternalCryptoBinding.ChaCha20Poly1305CipherJobConstructor; CheckPrimeJob: InternalCryptoBinding.CheckPrimeJobConstructor; DHBitsJob: InternalCryptoBinding.DHBitsJobConstructor;