Commit fb602d182d5 for nodejs

commit fb602d182d560841f30b9ec9f4b1ec124abe2106
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Tue Sep 22 22:23:31 2026 +0200

    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 <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66237
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Aviv Keller <me@aviv.sh>

diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js
index 3280ddff845..dd95748dcab 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 bf9eaedd85a..844f9dc94a3 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 3152362caa8..2e4bf9a8275 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 45071cded21..32bbcfbf0b7 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 5bf9d511542..63a9451b3cc 100644
--- a/lib/internal/crypto/diffiehellman.js
+++ b/lib/internal/crypto/diffiehellman.js
@@ -61,6 +61,7 @@ const {
   numBitsToBytes,
   toBuf,
   truncateToBitLength,
+  validateAlgorithm,
   kHandle,
 } = require('internal/crypto/util');

@@ -332,6 +333,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 0f1e0555202..6f566569a25 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 69bdcd38c17..12588b57263 100644
--- a/lib/internal/crypto/hash.js
+++ b/lib/internal/crypto/hash.js
@@ -27,6 +27,7 @@ const {
   normalizeHashName,
   numBitsToBytes,
   truncateToBitLength,
+  validateAlgorithm,
   validateMaxBufferLength,
   kHandle,
   getCachedHashId,
@@ -240,6 +241,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 8673f9b4681..bf8a296bc4b 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 3297297abd5..742e1ebe590 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 71238c4726f..581e9beb5d0 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 81ca6723429..56c58bf1262 100644
--- a/lib/internal/crypto/rsa.js
+++ b/lib/internal/crypto/rsa.js
@@ -31,6 +31,7 @@ const {
   getUsagesMask,
   jobPromise,
   normalizeHashName,
+  validateAlgorithm,
   validateMaxBufferLength,
   toUsagesSet,
 } = require('internal/crypto/util');
@@ -110,6 +111,7 @@ function rsaKeyGenerate(

   const allowedUsages = kUsages[name];
   const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name);
+  validateAlgorithm(algorithm, 'generateKey');
   const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent);

   const keyAlgorithm = {
diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js
index 537870f25e8..a9ad6dfb62f 100644
--- a/lib/internal/crypto/util.js
+++ b/lib/internal/crypto/util.js
@@ -820,8 +820,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.
@@ -857,6 +857,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);
@@ -1283,6 +1293,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 ae45d8a9ec2..7777774a217 100644
--- a/lib/internal/crypto/webcrypto.js
+++ b/lib/internal/crypto/webcrypto.js
@@ -41,6 +41,7 @@ const {
   codes: {
     ERR_ILLEGAL_CONSTRUCTOR,
     ERR_INVALID_ARG_TYPE,
+    ERR_INVALID_ARG_VALUE,
     ERR_INVALID_THIS,
   },
 } = require('internal/errors');
@@ -70,6 +71,7 @@ const {
   normalizeHashName,
   numBitsToBytes,
   prepareWebCryptoResult,
+  validateAlgorithm,
   validateMaxBufferLength,
   getOptionalByteLength,
 } = require('internal/crypto/util');
@@ -319,7 +321,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':
@@ -331,6 +335,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);
       }
@@ -1869,6 +1875,7 @@ class SubtleCrypto {
           normalizeAlgorithm(algorithm, 'get shared key length');
         sharedKeyLength = getSharedKeyLength(normalizedAlgorithm);
         normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey');
+        validateAlgorithm(normalizedAdditionalAlgorithm, 'importKey');
       } catch {
         return false;
       }
@@ -1897,6 +1904,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 1d35ce69f56..4addaf62bcc 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,
@@ -133,6 +136,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);
+}
+
 converters.BigInteger = webidl.Uint8Array;

 const dictRsaKeyGenParams = [
@@ -172,13 +210,13 @@ const dictRsaKeyGenParams = [
   },
 ];

-converters.RsaKeyGenParams = createDictionaryConverter(
+converters.RsaKeyGenParams = createAlgorithmDictionaryConverter(
   'RsaKeyGenParams', [
     dictAlgorithm,
     dictRsaKeyGenParams,
   ]);

-converters.RsaHashedKeyGenParams = createDictionaryConverter(
+converters.RsaHashedKeyGenParams = createAlgorithmDictionaryConverter(
   'RsaHashedKeyGenParams', [
     dictAlgorithm,
     dictRsaKeyGenParams,
@@ -191,7 +229,7 @@ converters.RsaHashedKeyGenParams = createDictionaryConverter(
     ],
   ]);

-converters.RsaHashedImportParams = createDictionaryConverter(
+converters.RsaHashedImportParams = createAlgorithmDictionaryConverter(
   'RsaHashedImportParams', [
     dictAlgorithm,
     [
@@ -205,7 +243,7 @@ converters.RsaHashedImportParams = createDictionaryConverter(

 converters.NamedCurve = converters.DOMString;

-converters.EcKeyImportParams = createDictionaryConverter(
+converters.EcKeyImportParams = createAlgorithmDictionaryConverter(
   'EcKeyImportParams', [
     dictAlgorithm,
     [
@@ -218,7 +256,7 @@ converters.EcKeyImportParams = createDictionaryConverter(
     ],
   ]);

-converters.EcKeyGenParams = createDictionaryConverter(
+converters.EcKeyGenParams = createAlgorithmDictionaryConverter(
   'EcKeyGenParams', [
     dictAlgorithm,
     [
@@ -231,7 +269,7 @@ converters.EcKeyGenParams = createDictionaryConverter(
     ],
   ]);

-converters.AesKeyGenParams = createDictionaryConverter(
+converters.AesKeyGenParams = createAlgorithmDictionaryConverter(
   'AesKeyGenParams', [
     dictAlgorithm,
     [
@@ -295,7 +333,7 @@ function validateCShakeCustomization(V) {
   validateMaxBufferLength(V, 'CShakeParams.customization', 512);
 }

-converters.RsaPssParams = createDictionaryConverter(
+converters.RsaPssParams = createAlgorithmDictionaryConverter(
   'RsaPssParams', [
     dictAlgorithm,
     [
@@ -308,7 +346,7 @@ converters.RsaPssParams = createDictionaryConverter(
     ],
   ]);

-converters.RsaOaepParams = createDictionaryConverter(
+converters.RsaOaepParams = createAlgorithmDictionaryConverter(
   'RsaOaepParams', [
     dictAlgorithm,
     [
@@ -319,7 +357,7 @@ converters.RsaOaepParams = createDictionaryConverter(
     ],
   ]);

-converters.EcdsaParams = createDictionaryConverter(
+converters.EcdsaParams = createAlgorithmDictionaryConverter(
   'EcdsaParams', [
     dictAlgorithm,
     [
@@ -344,7 +382,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,
       [
@@ -407,7 +445,7 @@ converters.JsonWebKey = createDictionaryConverter(
     simpleDomStringKey('priv'),
   ]);

-converters.HkdfParams = createDictionaryConverter(
+converters.HkdfParams = createAlgorithmDictionaryConverter(
   'HkdfParams', [
     dictAlgorithm,
     [
@@ -430,7 +468,7 @@ converters.HkdfParams = createDictionaryConverter(
     ],
   ]);

-converters.CShakeParams = createDictionaryConverter(
+converters.CShakeParams = createAlgorithmDictionaryConverter(
   'CShakeParams', [
     dictAlgorithm,
     [
@@ -454,7 +492,7 @@ converters.CShakeParams = createDictionaryConverter(
     ],
   ]);

-converters.Pbkdf2Params = createDictionaryConverter(
+converters.Pbkdf2Params = createAlgorithmDictionaryConverter(
   'Pbkdf2Params', [
     dictAlgorithm,
     [
@@ -477,7 +515,7 @@ converters.Pbkdf2Params = createDictionaryConverter(
     ],
   ]);

-converters.AesDerivedKeyParams = createDictionaryConverter(
+converters.AesDerivedKeyParams = createAlgorithmDictionaryConverter(
   'AesDerivedKeyParams', [
     dictAlgorithm,
     [
@@ -491,7 +529,7 @@ converters.AesDerivedKeyParams = createDictionaryConverter(
     ],
   ]);

-converters.AesCbcParams = createDictionaryConverter(
+converters.AesCbcParams = createAlgorithmDictionaryConverter(
   'AesCbcParams', [
     dictAlgorithm,
     [
@@ -504,7 +542,7 @@ converters.AesCbcParams = createDictionaryConverter(
     ],
   ]);

-converters.AeadParams = createDictionaryConverter(
+converters.AeadParams = createAlgorithmDictionaryConverter(
   'AeadParams', [
     dictAlgorithm,
     [
@@ -568,7 +606,7 @@ converters.AeadParams = createDictionaryConverter(
     ],
   ]);

-converters.AesCtrParams = createDictionaryConverter(
+converters.AesCtrParams = createAlgorithmDictionaryConverter(
   'AesCtrParams', [
     dictAlgorithm,
     [
@@ -596,7 +634,7 @@ converters.AesCtrParams = createDictionaryConverter(
 converters.CryptoKey = createInterfaceConverter(
   'CryptoKey', isCryptoKey);

-converters.EcdhKeyDeriveParams = createDictionaryConverter(
+converters.EcdhKeyDeriveParams = createAlgorithmDictionaryConverter(
   'EcdhKeyDeriveParams', [
     dictAlgorithm,
     [
@@ -618,7 +656,7 @@ converters.EcdhKeyDeriveParams = createDictionaryConverter(
     ],
   ]);

-converters.ContextParams = createDictionaryConverter(
+converters.ContextParams = createAlgorithmDictionaryConverter(
   'ContextParams', [
     dictAlgorithm,
     [
@@ -652,7 +690,7 @@ converters.ContextParams = createDictionaryConverter(
     ],
   ]);

-converters.Argon2Params = createDictionaryConverter(
+converters.Argon2Params = createAlgorithmDictionaryConverter(
   'Argon2Params', [
     dictAlgorithm,
     [
@@ -729,7 +767,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,
       [
@@ -743,7 +781,7 @@ for (let i = 0; i < kKmacDictionaries.length; i++) {
     ]);
 }

-converters.KmacParams = createDictionaryConverter(
+converters.KmacParams = createAlgorithmDictionaryConverter(
   'KmacParams', [
     dictAlgorithm,
     [
@@ -766,7 +804,7 @@ converters.KmacParams = createDictionaryConverter(
     ],
   ]);

-converters.KangarooTwelveParams = createDictionaryConverter(
+converters.KangarooTwelveParams = createAlgorithmDictionaryConverter(
   'KangarooTwelveParams', [
     dictAlgorithm,
     [
@@ -788,7 +826,7 @@ converters.KangarooTwelveParams = createDictionaryConverter(
     ],
   ]);

-converters.TurboShakeParams = createDictionaryConverter(
+converters.TurboShakeParams = createAlgorithmDictionaryConverter(
   'TurboShakeParams', [
     dictAlgorithm,
     [
@@ -818,6 +856,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 5f5f1761a4c..c050ad5c26b 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 00000000000..71459099356
--- /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 00000000000..20102c32e6a
--- /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 ae5d1d4b32d..e08551c65f3 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 3dcc4e7ca86..643343a77f4 100644
--- a/test/parallel/test-webcrypto-webidl.js
+++ b/test/parallel/test-webcrypto-webidl.js
@@ -643,7 +643,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',
   });
@@ -728,11 +730,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',
   });