Commit b48f24afbde for nodejs

commit b48f24afbdea6c540e9c11a49e8354ca72cb6e98
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Fri Sep 25 16:25:32 2026 +0200

    test: reduce expensive crypto test variants

    Skip redundant RSA keygen and PBKDF2 WPT variants during regular CI.
    Keep the full matrix when producing a WPT report.

    Use fewer scrypt, PBKDF2, and Argon2 derivations in parallel tests.
    Reuse RSA key material across OAEP hash and PSS salt tests.

    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    Assisted-by: Codex
    PR-URL: https://github.com/nodejs/node/pull/66244
    Reviewed-By: James M Snell <jasnell@gmail.com>
    Reviewed-By: Xuguang Mei <meixuguang@gmail.com>

diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js
index 447812877e9..1a53bef8fdd 100644
--- a/test/parallel/test-crypto-argon2.js
+++ b/test/parallel/test-crypto-argon2.js
@@ -11,13 +11,15 @@ if (!hasOpenSSL(3, 2))
 const assert = require('node:assert');
 const crypto = require('node:crypto');

-function runArgon2(algorithm, options) {
+function runArgon2(algorithm, options, testAsync = false) {
   const syncResult = crypto.argon2Sync(algorithm, options);

-  crypto.argon2(algorithm, options,
-                common.mustSucceed((asyncResult) => {
-                  assert.deepStrictEqual(asyncResult, syncResult);
-                }));
+  if (testAsync) {
+    crypto.argon2(algorithm, options,
+                  common.mustSucceed((asyncResult) => {
+                    assert.deepStrictEqual(asyncResult, syncResult);
+                  }));
+  }

   return syncResult;
 }
@@ -124,9 +126,11 @@ const bad = [
   assert.deepStrictEqual(omitted, explicitEmpty);
 }

-for (const [algorithm, overrides, expected] of good) {
+// The RFC vectors exercise both APIs for each Argon2 algorithm. Other vectors
+// check distinct options without repeating the derivation asynchronously.
+for (const [index, [algorithm, overrides, expected]] of good.entries()) {
   const parameters = { ...defaults, ...overrides };
-  const actual = runArgon2(algorithm, parameters);
+  const actual = runArgon2(algorithm, parameters, index < 3);
   assert.strictEqual(actual.toString('hex'), expected);
 }

diff --git a/test/parallel/test-crypto-pbkdf2.js b/test/parallel/test-crypto-pbkdf2.js
index 7cd1206f4f0..5083396f733 100644
--- a/test/parallel/test-crypto-pbkdf2.js
+++ b/test/parallel/test-crypto-pbkdf2.js
@@ -16,14 +16,16 @@ const validSalt = fips4 ? '0123456789abcdef' : 'salt';
 const validIterations = fips4 ? 1000 : 8;
 const validKeyLength = fips4 ? 16 : 8;

-function runPBKDF2(password, salt, iterations, keylen, hash) {
+function runPBKDF2(password, salt, iterations, keylen, hash, testAsync = true) {
   const syncResult =
     crypto.pbkdf2Sync(password, salt, iterations, keylen, hash);

-  crypto.pbkdf2(password, salt, iterations, keylen, hash,
-                common.mustSucceed((asyncResult) => {
-                  assert.deepStrictEqual(asyncResult, syncResult);
-                }));
+  if (testAsync) {
+    crypto.pbkdf2(password, salt, iterations, keylen, hash,
+                  common.mustSucceed((asyncResult) => {
+                    assert.deepStrictEqual(asyncResult, syncResult);
+                  }));
+  }

   return syncResult;
 }
@@ -47,20 +49,22 @@ function assertPBKDF2Fails(password, salt, iterations, keylen, hash) {
   );
 }

-function testPBKDF2(password, salt, iterations, keylen, expected, encoding) {
-  const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256');
+function testPBKDF2(password, salt, iterations, keylen, expected, encoding,
+                    testAsync = false) {
+  const actual = runPBKDF2(password, salt, iterations, keylen, 'sha256', testAsync);
   assert.strictEqual(actual.toString(encoding || 'latin1'), expected);
 }

 //
-// Test PBKDF2 with RFC 6070 test vectors (except #4)
+// Test PBKDF2 with RFC 6070 test vectors (except #4). Run one known answer
+// through the callback API; the rest only need synchronous derivation.
 //

 if (fips4) {
   testPBKDF2(validPassword, validSalt, validIterations, 32,
              '8514638175a45bc45eb1f22f04ff7d27' +
              'f4f8be480498c455ff4b494ce8d1e7d2',
-             'hex');
+             'hex', true);

   for (const args of [
     ['short', validSalt, validIterations],
@@ -82,7 +86,7 @@ if (fips4) {

   testPBKDF2('password', 'salt', 4096, 20,
              '\xc5\xe4\x78\xd5\x92\x88\xc8\x41\xaa\x53\x0d\xb6' +
-             '\x84\x5c\x4c\x8d\x96\x28\x93\xa0');
+             '\x84\x5c\x4c\x8d\x96\x28\x93\xa0', 'latin1', true);

   testPBKDF2('passwordPASSWORDpassword',
              'saltSALTsaltSALTsaltSALTsaltSALTsalt',
diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js
index 0f033e05d6d..e4a4a071558 100644
--- a/test/parallel/test-crypto-scrypt.js
+++ b/test/parallel/test-crypto-scrypt.js
@@ -43,6 +43,7 @@ const good = [
     N: 1024,
     p: 16,
     r: 8,
+    testAsync: true,
     expected:
         'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' +
         '2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640',
@@ -65,32 +66,11 @@ const good = [
     cost: 16,
     parallelization: 1,
     blockSize: 1,
+    testAsync: true,
     expected:
         '77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442' +
         'fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906',
   },
-  {
-    pass: 'password',
-    salt: 'NaCl',
-    keylen: 64,
-    cost: 1024,
-    parallelization: 16,
-    blockSize: 8,
-    expected:
-        'fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b373162' +
-        '2eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640',
-  },
-  {
-    pass: 'pleaseletmein',
-    salt: 'SodiumChloride',
-    keylen: 64,
-    cost: 16384,
-    parallelization: 1,
-    blockSize: 8,
-    expected:
-        '7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2' +
-        'd5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887',
-  },
 ];

 // Test vectors that contain invalid parameters.
@@ -171,9 +151,11 @@ if (isFips) {
     const { pass, salt, keylen, expected } = options;
     const actual = crypto.scryptSync(pass, salt, keylen, options);
     assert.strictEqual(actual.toString('hex'), expected);
-    crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => {
-      assert.strictEqual(actual.toString('hex'), expected);
-    }));
+    if (options.testAsync) {
+      crypto.scrypt(pass, salt, keylen, options, common.mustSucceed((actual) => {
+        assert.strictEqual(actual.toString('hex'), expected);
+      }));
+    }
   }
 }

@@ -215,12 +197,11 @@ for (const options of toobig) {
 }

 if (!isFips) {
-  const defaults = { N: 16384, p: 1, r: 8 };
-  const expected = crypto.scryptSync('pass', 'salt', 1, defaults);
-  const actual = crypto.scryptSync('pass', 'salt', 1);
-  assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex'));
-  crypto.scrypt('pass', 'salt', 1, common.mustSucceed((actual) => {
-    assert.deepStrictEqual(actual.toString('hex'), expected.toString('hex'));
+  const expected = '4cac4540';
+  const actual = crypto.scryptSync('pass', 'salt', 4);
+  assert.strictEqual(actual.toString('hex'), expected);
+  crypto.scrypt('pass', 'salt', 4, common.mustSucceed((actual) => {
+    assert.strictEqual(actual.toString('hex'), expected);
   }));
 }

@@ -241,9 +222,9 @@ for (const { args, expected } of badargs) {
   // Values for maxmem that do not fit in 32 bits but that are still safe
   // integers should be allowed.
   if (!isFips) {
-    crypto.scrypt('', '', 4, { maxmem: 2 ** 52 },
+    crypto.scrypt('', '', 4, { N: 16, maxmem: 2 ** 52 },
                   common.mustSucceed((actual) => {
-                    assert.strictEqual(actual.toString('hex'), 'd72c87d0');
+                    assert.strictEqual(actual.toString('hex'), 'e2b18837');
                   }));
   }

@@ -258,9 +239,12 @@ if (!isFips) {

   function testParameter(name, value) {
     let accessCount = 0;
+    // This regression checks getter access, so the derivation can be cheap.
+    const options = name === 'cost' ? { cost: 16 } : { N: 16 };

     // Find out how often the value is accessed.
     crypto.scryptSync('', '', 1, {
+      ...options,
       get [name]() {
         accessCount++;
         return value;
@@ -270,6 +254,7 @@ if (!isFips) {
     // Try to crash the process on the last access.
     assert.throws(() => {
       crypto.scryptSync('', '', 1, {
+        ...options,
         get [name]() {
           if (--accessCount === 0)
             return '';
@@ -282,7 +267,7 @@ if (!isFips) {
   }

   [
-    ['N', 16384], ['cost', 16384],
+    ['N', 16], ['cost', 16],
     ['r', 8], ['blockSize', 8],
     ['p', 1], ['parallelization', 1],
   ].forEach((arg) => testParameter(...arg));
@@ -292,17 +277,18 @@ if (!isFips) {
 // IsInt32() assertion. Assert that `-0` produces the same outcome as
 // `+0` (which differs by OpenSSL build).
 {
+  const options = { N: 16 };
   let posError;
   let posResult;
   try {
-    posResult = crypto.scryptSync('', '', 0);
+    posResult = crypto.scryptSync('', '', 0, options);
   } catch (err) {
     posError = err;
   }
   let negError;
   let negResult;
   try {
-    negResult = crypto.scryptSync('', '', -0);
+    negResult = crypto.scryptSync('', '', -0, options);
   } catch (err) {
     negError = err;
   }
@@ -314,9 +300,9 @@ if (!isFips) {

   if (isFips) {
     assert.throws(
-      () => crypto.scrypt('', '', -0, () => {}),
+      () => crypto.scrypt('', '', -0, options, () => {}),
       { code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS' });
   } else {
-    crypto.scrypt('', '', -0, common.mustCall());
+    crypto.scrypt('', '', -0, options, common.mustCall());
   }
 }
diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js
index 129b716c591..a7fbed77f25 100644
--- a/test/parallel/test-webcrypto-derivebits-argon2.js
+++ b/test/parallel/test-webcrypto-derivebits-argon2.js
@@ -80,7 +80,7 @@ for (const { algorithm, length, password, params, tag } of vectors) {
       const result = await subtle.deriveBits(parameters, key, length);
       assert.deepStrictEqual(result, tag);
     }
-    {
+    if (algorithm === 'Argon2id') {
       const derivedKeyType = { name: 'HMAC', length, hash: 'SHA-256' };

       const key = createSecretKey(password)
diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js
index 0f3341db64b..8c851682432 100644
--- a/test/parallel/test-webcrypto-encrypt-decrypt.js
+++ b/test/parallel/test-webcrypto-encrypt-decrypt.js
@@ -13,95 +13,55 @@ const { subtle } = globalThis.crypto;
 // This is only a partial test. The WebCrypto Web Platform Tests
 // will provide much greater coverage.

-// Test Encrypt/Decrypt RSA-OAEP w/ SHA-2
-{
+async function testRSAOaep(publicKey, privateKey) {
   const buf = globalThis.crypto.getRandomValues(new Uint8Array(50));
-
-  async function test() {
-    const ec = new TextEncoder();
-    const { publicKey, privateKey } = await subtle.generateKey({
-      name: 'RSA-OAEP',
-      modulusLength: 2048,
-      publicExponent: new Uint8Array([1, 0, 1]),
-      hash: 'SHA-384',
-    }, true, ['encrypt', 'decrypt']);
-
-    const ciphertext = await subtle.encrypt({
-      name: 'RSA-OAEP',
-      label: ec.encode('a label')
-    }, publicKey, buf);
-
-    const plaintext = await subtle.decrypt({
-      name: 'RSA-OAEP',
-      label: ec.encode('a label')
-    }, privateKey, ciphertext);
-
-    assert.strictEqual(
-      Buffer.from(plaintext).toString('hex'),
-      Buffer.from(buf).toString('hex'));
-
-    await assert.rejects(() => subtle.encrypt({
-      name: 'RSA-OAEP',
-    }, privateKey, buf), {
-      name: 'InvalidAccessError',
-      message: 'Unable to use this key to encrypt'
-    });
-
-    await assert.rejects(() => subtle.decrypt({
-      name: 'RSA-OAEP',
-    }, publicKey, ciphertext), {
-      name: 'InvalidAccessError',
-      message: 'Unable to use this key to decrypt'
-    });
-  }
-
-  test().then(common.mustCall());
+  const label = new TextEncoder().encode('a label');
+  const ciphertext = await subtle.encrypt({ name: 'RSA-OAEP', label }, publicKey, buf);
+  const plaintext = await subtle.decrypt({ name: 'RSA-OAEP', label }, privateKey, ciphertext);
+
+  assert.strictEqual(
+    Buffer.from(plaintext).toString('hex'),
+    Buffer.from(buf).toString('hex'));
+
+  await assert.rejects(() => subtle.encrypt({
+    name: 'RSA-OAEP',
+  }, privateKey, buf), {
+    name: 'InvalidAccessError',
+    message: 'Unable to use this key to encrypt'
+  });
+
+  await assert.rejects(() => subtle.decrypt({
+    name: 'RSA-OAEP',
+  }, publicKey, ciphertext), {
+    name: 'InvalidAccessError',
+    message: 'Unable to use this key to decrypt'
+  });
 }

-// Test Encrypt/Decrypt RSA-OAEP w/ SHA-3
-if (!isBoringSSL) {
-  const buf = globalThis.crypto.getRandomValues(new Uint8Array(50));
-
-  async function test() {
-    const ec = new TextEncoder();
-    const { publicKey, privateKey } = await subtle.generateKey({
-      name: 'RSA-OAEP',
-      modulusLength: 2048,
-      publicExponent: new Uint8Array([1, 0, 1]),
-      hash: 'SHA3-384',
-    }, true, ['encrypt', 'decrypt']);
-
-    const ciphertext = await subtle.encrypt({
-      name: 'RSA-OAEP',
-      label: ec.encode('a label')
-    }, publicKey, buf);
-
-    const plaintext = await subtle.decrypt({
-      name: 'RSA-OAEP',
-      label: ec.encode('a label')
-    }, privateKey, ciphertext);
-
-    assert.strictEqual(
-      Buffer.from(plaintext).toString('hex'),
-      Buffer.from(buf).toString('hex'));
-
-    await assert.rejects(() => subtle.encrypt({
-      name: 'RSA-OAEP',
-    }, privateKey, buf), {
-      name: 'InvalidAccessError',
-      message: 'Unable to use this key to encrypt'
-    });
-
-    await assert.rejects(() => subtle.decrypt({
-      name: 'RSA-OAEP',
-    }, publicKey, ciphertext), {
-      name: 'InvalidAccessError',
-      message: 'Unable to use this key to decrypt'
-    });
+(async function() {
+  const { publicKey, privateKey } = await subtle.generateKey({
+    name: 'RSA-OAEP',
+    modulusLength: 2048,
+    publicExponent: new Uint8Array([1, 0, 1]),
+    hash: 'SHA-384',
+  }, true, ['encrypt', 'decrypt']);
+
+  await testRSAOaep(publicKey, privateKey);
+
+  if (!isBoringSSL) {
+    // Import the same key material with SHA-3 to avoid another RSA keygen.
+    const [spki, pkcs8] = await Promise.all([
+      subtle.exportKey('spki', publicKey),
+      subtle.exportKey('pkcs8', privateKey),
+    ]);
+    const algorithm = { name: 'RSA-OAEP', hash: 'SHA3-384' };
+    const [sha3PublicKey, sha3PrivateKey] = await Promise.all([
+      subtle.importKey('spki', spki, algorithm, false, ['encrypt']),
+      subtle.importKey('pkcs8', pkcs8, algorithm, false, ['decrypt']),
+    ]);
+    await testRSAOaep(sha3PublicKey, sha3PrivateKey);
   }
-
-  test().then(common.mustCall());
-}
+})().then(common.mustCall());

 // Test Encrypt/Decrypt AES-CTR
 {
diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js
index 5b5fc7a69e8..201d28fbf1f 100644
--- a/test/parallel/test-webcrypto-sign-verify-rsa.js
+++ b/test/parallel/test-webcrypto-sign-verify-rsa.js
@@ -214,13 +214,11 @@ async function testFipsSignRejected({
     { name: 'OperationError' });
 }

-async function testSaltLength(keyLength, hash, hLen) {
-  const { publicKey, privateKey } = await subtle.generateKey({
-    name: 'RSA-PSS',
-    modulusLength: keyLength,
-    publicExponent: new Uint8Array([1, 0, 1]),
-    hash,
-  }, false, ['sign', 'verify']);
+async function testSaltLength(keyLength, hash, hLen, spki, pkcs8) {
+  const [publicKey, privateKey] = await Promise.all([
+    subtle.importKey('spki', spki, { name: 'RSA-PSS', hash }, false, ['verify']),
+    subtle.importKey('pkcs8', pkcs8, { name: 'RSA-PSS', hash }, false, ['sign']),
+  ]);

   const data = Buffer.from('Hello, world!');
   const max = keyLength / 8 - hLen - 2;
@@ -249,6 +247,39 @@ async function testSaltLength(keyLength, hash, hLen) {
   }
 }

+async function testSaltLengths(keyLength) {
+  // Reuse the same RSA key material across hashes. The salt boundary depends
+  // on the modulus length and digest size, not on a newly generated modulus.
+  const { publicKey, privateKey } = await subtle.generateKey({
+    name: 'RSA-PSS',
+    modulusLength: keyLength,
+    publicExponent: new Uint8Array([1, 0, 1]),
+    hash: 'SHA-256',
+  }, true, ['sign', 'verify']);
+  const [spki, pkcs8] = await Promise.all([
+    subtle.exportKey('spki', publicKey),
+    subtle.exportKey('pkcs8', privateKey),
+  ]);
+
+  const variations = [];
+  for (const [hash, hLen] of [
+    ['SHA-1', 20],
+    ['SHA-256', 32],
+    ['SHA-384', 48],
+    ['SHA-512', 64],
+    ...(!isBoringSSL ? [
+      ['SHA3-256', 32],
+      ['SHA3-384', 48],
+      ['SHA3-512', 64],
+    ] : []),
+  ]) {
+    if (rejectsSha1Signing && hash === 'SHA-1')
+      continue;
+    variations.push(testSaltLength(keyLength, hash, hLen, spki, pkcs8));
+  }
+  await Promise.all(variations);
+}
+
 (async function() {
   const variations = [];

@@ -263,23 +294,8 @@ async function testSaltLength(keyLength, hash, hLen) {
       testFipsSignRejected(vector) : testSign(vector));
   });

-  for (const keyLength of fips3 ? [2048] : [1024, 2048]) {
-    for (const [hash, hLen] of [
-      ['SHA-1', 20],
-      ['SHA-256', 32],
-      ['SHA-384', 48],
-      ['SHA-512', 64],
-      ...(!isBoringSSL ? [
-        ['SHA3-256', 32],
-        ['SHA3-384', 48],
-        ['SHA3-512', 64],
-      ] : []),
-    ]) {
-      if (rejectsSha1Signing && hash === 'SHA-1')
-        continue;
-      variations.push(testSaltLength(keyLength, hash, hLen));
-    }
-  }
+  for (const keyLength of fips3 ? [2048] : [1024, 2048])
+    variations.push(testSaltLengths(keyLength));

   await Promise.all(variations);
 })().then(common.mustCall());
diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs
index ff618016f3d..e2225c52d73 100644
--- a/test/wpt/status/WebCryptoAPI.cjs
+++ b/test/wpt/status/WebCryptoAPI.cjs
@@ -194,6 +194,47 @@ if (hasFIPS(3, 5)) {
     ]);
 }

+if (process.env.WPT_REPORT === undefined) {
+  // Keep the full matrix in both globals when producing a WPT report. Regular
+  // CI runs these expensive suites in the window global only.
+  conditionalFileSkips['derive_bits_keys/pbkdf2.https.any.worker.html'] = {
+    'skip': 'Expensive worker matrix runs with WPT_REPORT',
+  };
+
+  // Retain one 100000-iteration deriveKey per PBKDF2 vector; deriveBits still
+  // checks every password, salt, and hash, and cheaper vectors cover all
+  // derived key types.
+  skipSubtests([
+    'derive_bits_keys/pbkdf2.https.any.html',
+    /^Derived key of type (?!name: AES-CBC length: 256\s+using\b).* using .*, SHA-(?:1|256|384|512), with 100000 iterations$/,
+  ]);
+
+  // RSA name variants are grouped in the order uppercase, lowercase, mixed.
+  // Keep the uppercase group and skip variants containing only the other two.
+  for (const [name, firstSkipped, last] of [
+    ['RSA-PSS', 21, 31],
+    ['RSASSA-PKCS1-v1_5', 21, 31],
+    ['RSA-OAEP', 61, 151],
+  ]) {
+    conditionalFileSkips[`generateKey/successes_${name}.https.any.worker.html`] = {
+      'skip': 'Expensive worker matrix runs with WPT_REPORT',
+    };
+    const prefix = `generateKey/successes_${name}.https.any.html?`;
+    skipSubtests([
+      `${prefix}${firstSkipped - 10}-${firstSkipped - 1}`,
+      /name: rsa/,
+    ]);
+    for (let start = firstSkipped; start < last; start += 10) {
+      conditionalFileSkips[`${prefix}${start}-${start + 9}`] = {
+        'skip': 'Redundant RSA key generation in regular CI',
+      };
+    }
+    conditionalFileSkips[`${prefix}${last}-last`] = {
+      'skip': 'Redundant RSA key generation in regular CI',
+    };
+  }
+}
+
 if (hasFIPS(4)) {
   skipSubtests(
     [