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(
[