From 2916d547a9adef9ad568ffc72a105389c1c01140 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 11 Aug 2026 22:30:22 +0200 Subject: [PATCH 1/3] crypto: improve SubtleCrypto.supports() accuracy Constrain context parameters, ML-KEM derived-key imports, HKDF output lengths, and RSA key generation. Signed-off-by: Filip Skokan --- lib/internal/crypto/hkdf.js | 10 ++- lib/internal/crypto/rsa.js | 13 +-- lib/internal/crypto/util.js | 12 ++- lib/internal/crypto/webcrypto.js | 85 +++++++++++++------ lib/internal/crypto/webidl.js | 40 ++++++++- test/fixtures/webcrypto/supports-level-2.mjs | 32 +++++++ .../webcrypto/supports-modern-algorithms.mjs | 16 ++++ .../webcrypto/supports-secure-curves.mjs | 2 + .../test-webcrypto-derivebits-hkdf.js | 21 +++++ test/parallel/test-webcrypto-keygen.js | 19 ++++- ...-webcrypto-promise-prototype-pollution.mjs | 13 ++- .../test-webcrypto-sign-verify-eddsa.js | 20 ++--- .../test-webcrypto-sign-verify-ml-dsa.js | 16 ++-- test/parallel/test-webcrypto-supports.mjs | 36 ++++++-- test/parallel/test-webcrypto-util.js | 10 ++- 15 files changed, 264 insertions(+), 81 deletions(-) diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index c9b868e23af4..d55968907541 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -22,6 +22,7 @@ const { const { kMaxLength } = require('buffer'); const { + getDigestSizeInBytes, jobPromise, normalizeHashName, toBuf, @@ -141,7 +142,7 @@ function hkdfSync(hash, key, salt, info, length) { return bits; } -function validateHkdfDeriveBitsLength(length) { +function validateHkdfDeriveBitsLength(length, hash) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); if (length % 8) { @@ -149,11 +150,16 @@ function validateHkdfDeriveBitsLength(length) { 'length must be a multiple of 8', 'OperationError'); } + if (length > 255 * getDigestSizeInBytes(hash.name) * 8) { + throw lazyDOMException( + 'length exceeds the maximum derived bit length', + 'OperationError'); + } } function hkdfDeriveBits(algorithm, baseKey, length) { - validateHkdfDeriveBitsLength(length); const { hash, salt, info } = algorithm; + validateHkdfDeriveBitsLength(length, hash); if (length === 0) return PromiseResolve(new ArrayBuffer(0)); diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d153ab664bd8..846b6b7bf748 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -103,12 +103,6 @@ function rsaKeyGenerate( extractable, usages, ) { - const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent); - if (publicExponentConverted === undefined) { - throw lazyDOMException( - 'The publicExponent must be equivalent to an unsigned 32-bit value', - 'OperationError'); - } const { name, modulusLength, @@ -118,6 +112,7 @@ function rsaKeyGenerate( const allowedUsages = kUsages[name]; const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); + const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent); const keyAlgorithm = { name, @@ -126,12 +121,6 @@ function rsaKeyGenerate( hash, }; - if (publicExponentConverted < 3 || publicExponentConverted % 2 === 0) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - 'OperationError'); - } - const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); validateUsagesNotEmpty(keyUsages.private); diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 39dea84a83b6..3c52feebea68 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -349,6 +349,7 @@ const kAlgorithmDefinitions = { 'importKey': null, 'encapsulate': null, 'decapsulate': null, + 'get shared key length': null, }, 'ML-KEM-768': { 'generateKey': null, @@ -356,6 +357,7 @@ const kAlgorithmDefinitions = { 'importKey': null, 'encapsulate': null, 'decapsulate': null, + 'get shared key length': null, }, 'ML-KEM-1024': { 'generateKey': null, @@ -363,6 +365,7 @@ const kAlgorithmDefinitions = { 'importKey': null, 'encapsulate': null, 'decapsulate': null, + 'get shared key length': null, }, 'PBKDF2': { 'importKey': null, @@ -894,15 +897,18 @@ function jobPromiseThen(promise, onFulfilled, onRejected) { // an unsigned int from a Buffer are not adequate. The implementation // here is adapted from the chromium implementation here: // https://github.com/chromium/chromium/blob/HEAD/third_party/blink/public/platform/web_crypto_algorithm_params.h, but ported to JavaScript -// Returns undefined if the conversion was unsuccessful. +// Throws an OperationError if the value does not fit in an unsigned 32-bit integer. function bigIntArrayToUnsignedInt(input) { let result = 0; const length = TypedArrayPrototypeGetLength(input); for (let n = 0; n < length; ++n) { const n_reversed = length - n - 1; - if (n_reversed >= 4 && input[n]) - return; // Too large + if (n_reversed >= 4 && input[n]) { + throw lazyDOMException( + 'algorithm.publicExponent must fit in an unsigned 32-bit integer', + 'OperationError'); + } result |= input[n] << 8 * n_reversed; } diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index c2803cca1372..55953d8e4745 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -348,6 +348,54 @@ function getKeyLength({ name, length, hash }) { } } +function getSharedKeyLength({ name }) { + switch (name) { + case 'ML-KEM-512': + // Fall through + case 'ML-KEM-768': + // Fall through + case 'ML-KEM-1024': + return 256; + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ + } +} + +function canImportRawSecret(algorithm, sharedKeyLength) { + switch (algorithm.name) { + case 'AES-OCB': + case 'AES-KW': + case 'AES-GCM': + case 'AES-CTR': + case 'AES-CBC': + return sharedKeyLength === 128 || + sharedKeyLength === 192 || + sharedKeyLength === 256; + case 'ChaCha20-Poly1305': + return sharedKeyLength === 256; + case 'HKDF': + case 'PBKDF2': + case 'Argon2i': + case 'Argon2d': + case 'Argon2id': + return true; + case 'HMAC': + if (sharedKeyLength === 0) + return false; + // Fall through + case 'KMAC128': + case 'KMAC256': + return algorithm.length === undefined || + numBitsToBytes(algorithm.length) * 8 === sharedKeyLength; + default: + return false; + } +} + function deriveKey( algorithm, baseKey, @@ -1741,37 +1789,19 @@ class SubtleCrypto { }, ); + let sharedKeyLength; let normalizedAdditionalAlgorithm; try { + const normalizedAlgorithm = + normalizeAlgorithm(algorithm, 'get shared key length'); + sharedKeyLength = getSharedKeyLength(normalizedAlgorithm); normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey'); } catch { return false; } - switch (normalizedAdditionalAlgorithm.name) { - case 'AES-OCB': - case 'AES-KW': - case 'AES-GCM': - case 'AES-CTR': - case 'AES-CBC': - case 'ChaCha20-Poly1305': - case 'HKDF': - case 'PBKDF2': - case 'Argon2i': - case 'Argon2d': - case 'Argon2id': - break; - case 'HMAC': - case 'KMAC128': - case 'KMAC256': - if (normalizedAdditionalAlgorithm.length === undefined || - numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) { - break; - } - return false; - default: - return false; - } + if (!canImportRawSecret(normalizedAdditionalAlgorithm, sharedKeyLength)) + return false; } try { @@ -1807,8 +1837,6 @@ function check(op, alg, length) { } switch (op) { - case 'decapsulate': - case 'decrypt': case 'digest': { if ((normalizedAlgorithm.name === 'cSHAKE128' || normalizedAlgorithm.name === 'cSHAKE256') && @@ -1818,6 +1846,8 @@ function check(op, alg, length) { } return true; } + case 'decapsulate': + case 'decrypt': case 'encapsulate': case 'encrypt': case 'exportKey': @@ -1829,7 +1859,8 @@ function check(op, alg, length) { return true; case 'deriveBits': { if (normalizedAlgorithm.name === 'HKDF') { - require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); + require('internal/crypto/hkdf') + .validateHkdfDeriveBitsLength(length, normalizedAlgorithm.hash); } if (normalizedAlgorithm.name === 'PBKDF2') { diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 5661921a624c..9474aa4d18eb 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -23,6 +23,7 @@ const { getCryptoKeyType, } = require('internal/crypto/keys'); const { + bigIntArrayToUnsignedInt, validateMaxBufferLength, getBufferSourceByteLength, getBufferSourceBytes, @@ -41,6 +42,8 @@ const { type, } = require('internal/webidl'); +const kRsaKeyGenMinimumModulusLength = isFips === 1 ? 2048 : 512; + function validateByteLength(buf, name, target) { if (getBufferSourceByteLength(buf) !== target) { throw lazyDOMException( @@ -152,11 +155,33 @@ const dictRsaKeyGenParams = [ key: 'modulusLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), + validator: (modulusLength) => { + if (modulusLength < kRsaKeyGenMinimumModulusLength) { + throw lazyDOMException( + `algorithm.modulusLength must be at least ${kRsaKeyGenMinimumModulusLength}`, + 'OperationError'); + } + }, required: true, }, { key: 'publicExponent', converter: converters.BigInteger, + validator: (publicExponent) => { + const converted = bigIntArrayToUnsignedInt(publicExponent); + + if (converted < 3) { + throw lazyDOMException( + 'algorithm.publicExponent must be at least 3', + 'OperationError'); + } + + if (converted % 2 === 0) { + throw lazyDOMException( + 'algorithm.publicExponent must be odd', + 'OperationError'); + } + }, required: true, }, ]; @@ -625,20 +650,27 @@ converters.ContextParams = createDictionaryConverter( key: 'context', converter: converters.BufferSource, validator(V, dict) { + const validateLength = (V) => + validateMaxBufferLength(V, 'ContextParams.context', 255); + if (process.features.openssl_is_boringssl) { - this.validator = undefined; + this.validator = validateLength; } else { let { 0: major, 1: minor } = StringPrototypeSplit(process.versions.openssl, '.'); major = NumberParseInt(major, 10); minor = NumberParseInt(minor, 10); if (major > 3 || (major === 3 && minor >= 2)) { - this.validator = undefined; + this.validator = validateLength; } else { - this.validator = validateZeroLength('ContextParams.context'); - this.validator(V, dict); + const validateEmpty = validateZeroLength('ContextParams.context'); + this.validator = (V, dict) => { + validateLength(V); + validateEmpty(V, dict); + }; } } + this.validator(V, dict); }, }, ], diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 674e4cf8c6de..3d0f8a630394 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -1,4 +1,7 @@ +import { getFips } from 'node:crypto'; + const { subtle } = globalThis.crypto; +const RSA_MINIMUM_MODULUS_LENGTH = getFips() === 1 ? 2048 : 512; const RSA_KEY_GEN = { modulusLength: 2048, @@ -66,6 +69,30 @@ export const vectors = { [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }], + [true, { + name: 'RSA-PSS', + hash: 'SHA-256', + modulusLength: RSA_MINIMUM_MODULUS_LENGTH, + publicExponent: new Uint8Array([1, 0, 1]), + }], + [false, { + name: 'RSASSA-PKCS1-v1_5', + hash: 'SHA-256', + modulusLength: RSA_MINIMUM_MODULUS_LENGTH - 1, + publicExponent: new Uint8Array([1, 0, 1]), + }], + [false, { + name: 'RSA-PSS', + hash: 'SHA-256', + ...RSA_KEY_GEN, + publicExponent: new Uint8Array([2]), + }], + [false, { + name: 'RSA-OAEP', + hash: 'SHA-256', + ...RSA_KEY_GEN, + publicExponent: new Uint8Array([1, 0, 0, 0, 1]), + }], [true, { name: 'ECDSA', namedCurve: 'P-256' }], [false, { name: 'ECDSA', namedCurve: 'X25519' }], [true, { name: 'AES-CTR', length: 128 }], @@ -146,6 +173,8 @@ export const vectors = { 'deriveBits': [ [true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], [true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0], + [true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 65280], + [false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 65288], [false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, null], [false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 7], [false, { name: 'HKDF', hash: 'Invalid', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8], @@ -234,4 +263,7 @@ export const vectors = { 'get key length': [ [false, { name: 'HMAC', hash: 'SHA-256' }], ], + 'get shared key length': [ + [false, 'ML-KEM-768'], + ], }; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index acb0e249dd38..4b44e02d479d 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -70,6 +70,12 @@ export const vectors = { [pqc, { name: 'ML-DSA-44', context: Buffer.alloc(32) }], [pqc, { name: 'ML-DSA-65', context: Buffer.alloc(32) }], [pqc, { name: 'ML-DSA-87', context: Buffer.alloc(32) }], + [pqc, { name: 'ML-DSA-44', context: Buffer.alloc(255) }], + [pqc, { name: 'ML-DSA-65', context: Buffer.alloc(255) }], + [pqc, { name: 'ML-DSA-87', context: Buffer.alloc(255) }], + [false, { name: 'ML-DSA-44', context: Buffer.alloc(256) }], + [false, { name: 'ML-DSA-65', context: Buffer.alloc(256) }], + [false, { name: 'ML-DSA-87', context: Buffer.alloc(256) }], [false, 'Argon2d'], [false, 'Argon2i'], [false, 'Argon2id'], @@ -225,9 +231,14 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [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, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 248 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 512 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 257 }], [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], + [false, 'ML-KEM-768', 'Ed25519'], ], 'decapsulateBits': [ [pqc && !boringSSL, 'ML-KEM-512'], @@ -246,8 +257,13 @@ export const vectors = { [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], [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, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 248 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 512 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 257 }], [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], + [false, 'ML-KEM-768', 'Ed25519'], ], }; diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index 56bb89c28afa..a3a84f6c9f9a 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -19,6 +19,8 @@ export const vectors = { [!boringSSL, 'Ed448'], [!boringSSL, { name: 'Ed448', context: Buffer.alloc(0) }], [!boringSSL && supportsContext, { name: 'Ed448', context: Buffer.alloc(32) }], + [!boringSSL && supportsContext, { name: 'Ed448', context: Buffer.alloc(255) }], + [false, { name: 'Ed448', context: Buffer.alloc(256) }], ], 'generateKey': [ [!boringSSL, 'X448'], diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index d2057d1f782e..539440ea7c31 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -639,6 +639,27 @@ async function testWrongKeyType( assert.deepStrictEqual(bits, new ArrayBuffer(0)); })().then(common.mustCall()); +// HKDF output is limited to 255 digest blocks. +(async function() { + const key = await crypto.subtle.importKey( + 'raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + const algorithm = { + name: 'HKDF', + hash: 'SHA-256', + info: new Uint8Array(0), + salt: new Uint8Array(0), + }; + + const bits = await crypto.subtle.deriveBits(algorithm, key, 65280); + assert.strictEqual(bits.byteLength, 8160); + + await assert.rejects( + crypto.subtle.deriveBits(algorithm, key, 65288), { + name: 'OperationError', + message: 'length exceeds the maximum derived bit length', + }); +})().then(common.mustCall()); + // OpenSSL limits info to 1024 bytes (async function() { const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 6ea2579ae041..6e264031c4d1 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -18,6 +18,7 @@ const { const { subtle } = globalThis.crypto; const fips3 = hasFIPS(3); const fips35 = hasFIPS(3, 5); +const rsaMinimumModulusLength = getFips() === 1 ? 2048 : 512; const { bigIntArrayToUnsignedBigInt } = require('internal/crypto/util'); @@ -432,7 +433,7 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { subtle.generateKey( { name, modulusLength, publicExponent: new Uint8Array([1, 1, 1, 1, 1]), hash }, true, usages), { - message: /The publicExponent must be equivalent to an unsigned 32-bit value/, + message: 'algorithm.publicExponent must fit in an unsigned 32-bit integer', name: 'OperationError', }); @@ -456,16 +457,30 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { }); })); - await Promise.all([[1], [1, 0, 0]].map((publicExponent) => { + await Promise.all([ + [[1], 'algorithm.publicExponent must be at least 3'], + [[1, 0, 0], 'algorithm.publicExponent must be odd'], + ].map(({ 0: publicExponent, 1: message }) => { return assert.rejects(subtle.generateKey({ name, modulusLength, publicExponent: new Uint8Array(publicExponent), hash }, true, usages), { + message, name: 'OperationError', }); })); + + await assert.rejects(subtle.generateKey({ + name, + modulusLength: rsaMinimumModulusLength - 1, + publicExponent: new Uint8Array([3]), + hash, + }, true, usages), { + message: `algorithm.modulusLength must be at least ${rsaMinimumModulusLength}`, + name: 'OperationError', + }); } const kTests = [ diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index 6d8a3fa3df9f..f25acd356e33 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -996,7 +996,8 @@ const keyLengthTargets = { function getSupportedAlgorithmOperations() { const algorithms = new Map(); for (const operation of Object.keys(kSupportedAlgorithms)) { - if (operation === 'get key length') + if (operation === 'get key length' || + operation === 'get shared key length') continue; for (const name of Object.keys(kSupportedAlgorithms[operation])) { if (!algorithms.has(name)) @@ -1029,6 +1030,7 @@ const operationOrder = [ const coveredOperations = new Set([ ...operationOrder, 'get key length', + 'get shared key length', ]); for (const operation of Object.keys(kSupportedAlgorithms)) { @@ -1037,6 +1039,15 @@ for (const operation of Object.keys(kSupportedAlgorithms)) { `missing prototype pollution operation coverage for ${operation}`); } +const sharedKeyLengthAlgorithms = + Object.keys(kSupportedAlgorithms['get shared key length'] ?? {}); +assert.deepStrictEqual( + sharedKeyLengthAlgorithms, + Object.keys(kSupportedAlgorithms.encapsulate ?? {})); +assert.deepStrictEqual( + sharedKeyLengthAlgorithms, + Object.keys(kSupportedAlgorithms.decapsulate ?? {})); + const supportedAlgorithms = getSupportedAlgorithmOperations(); for (const [name, operations] of supportedAlgorithms) { const fixture = fixtures.get(name); diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index ad587a1220e0..e34a3d43d2a3 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -152,14 +152,12 @@ async function testVerify({ name, message: /Key algorithm mismatch/ }); - if (name === 'Ed448' && supportsContext) { + if (name === 'Ed448') { // Test failure when too long context await assert.rejects( - subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } @@ -278,14 +276,12 @@ async function testSign({ name, message: /Key algorithm mismatch/ }); - if (name === 'Ed448' && supportsContext) { + if (name === 'Ed448') { // Test failure when too long context await assert.rejects( - subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } } diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 67c85f92e4e2..ba6eab08efb1 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -101,11 +101,9 @@ async function testVerify({ name, // Test failure when too long context await assert.rejects( - subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.verify({ name, context: new Uint8Array(256) }, publicKey, signature, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); // Test failure when signature is altered @@ -209,11 +207,9 @@ async function testSign({ name, // Test failure when too long context await assert.rejects( - subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause.message, 'context string must be at most 255 bytes'); - return true; + subtle.sign({ name, context: new Uint8Array(256) }, privateKey, data), { + name: 'OperationError', + message: 'ContextParams.context must be at most 255 bytes', }); } diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index 2bc9d589b478..6c3b2c640fd7 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -66,21 +66,45 @@ function supportsRawSecret(alg) { return false; } -function supportsEncapsulatedRawSecret(alg) { +function getSharedKeyLength(alg) { + switch (alg?.name?.toLowerCase?.() ?? alg?.toLowerCase?.()) { + case 'ml-kem-512': + case 'ml-kem-768': + case 'ml-kem-1024': + return 256; + } +} + +function supportsEncapsulatedRawSecret(encapsulationAlgorithm, alg) { if (!supportsRawSecret(alg)) return false; - switch (alg?.name?.toLowerCase?.()) { + + const sharedKeyLength = getSharedKeyLength(encapsulationAlgorithm); + const name = alg?.name?.toLowerCase?.() ?? alg?.toLowerCase?.(); + if (name?.startsWith('aes')) { + return sharedKeyLength === 128 || + sharedKeyLength === 192 || + sharedKeyLength === 256; + } + + switch (name) { + case 'chacha20-poly1305': + return sharedKeyLength === 256; case 'hmac': + if (sharedKeyLength === 0) return false; + // Fall through case 'kmac128': case 'kmac256': - return typeof alg.length !== 'number' || Math.ceil(alg.length / 8) === 32; + return typeof alg !== 'object' || + typeof alg.length !== 'number' || + Math.ceil(alg.length / 8) * 8 === sharedKeyLength; default: - return true; + return sharedKeyLength !== undefined; } } for (const encap of vectors.encapsulateBits) { for (const imp of vectors.importKey) { - if (supportsEncapsulatedRawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(encap[1], imp[1])) { vectors.encapsulateKey.push([encap[0] && imp[0], encap[1], imp[1]]); } else { vectors.encapsulateKey.push([false, encap[1], imp[1]]); @@ -90,7 +114,7 @@ for (const encap of vectors.encapsulateBits) { for (const decap of vectors.decapsulateBits) { for (const imp of vectors.importKey) { - if (supportsEncapsulatedRawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(decap[1], imp[1])) { vectors.decapsulateKey.push([decap[0] && imp[0], decap[1], imp[1]]); } else { vectors.decapsulateKey.push([false, decap[1], imp[1]]); diff --git a/test/parallel/test-webcrypto-util.js b/test/parallel/test-webcrypto-util.js index 9763acfb71e0..c0c565a15f3d 100644 --- a/test/parallel/test-webcrypto-util.js +++ b/test/parallel/test-webcrypto-util.js @@ -27,8 +27,14 @@ const { bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 1])), 65537); assert.strictEqual( - bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 0, 0, 0])), - undefined); + bigIntArrayToUnsignedInt(new Uint8Array([0, 0, 1, 0, 1])), + 65537); + assert.throws( + () => bigIntArrayToUnsignedInt(new Uint8Array([1, 0, 0, 0, 0])), + { + name: 'OperationError', + message: 'algorithm.publicExponent must fit in an unsigned 32-bit integer', + }); } { From 6a1ac54cd295f95fee76c08d25699706ffa7b246 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 11 Aug 2026 23:40:37 +0200 Subject: [PATCH 2/3] fixup! crypto: improve SubtleCrypto.supports() accuracy --- lib/internal/crypto/webidl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 9474aa4d18eb..7f73d1bd66de 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -42,7 +42,7 @@ const { type, } = require('internal/webidl'); -const kRsaKeyGenMinimumModulusLength = isFips === 1 ? 2048 : 512; +const kRsaKeyGenMinimumModulusLength = isFips ? 2048 : 512; function validateByteLength(buf, name, target) { if (getBufferSourceByteLength(buf) !== target) { From 51bf88d5bb50c14ff7924347d091fe4c5839631b Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 12 Aug 2026 00:23:35 +0200 Subject: [PATCH 3/3] fixup! crypto: improve SubtleCrypto.supports() accuracy --- test/parallel/test-webcrypto-sign-verify-rsa.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 3e706941595d..62e9cbe7826a 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -259,18 +259,6 @@ async function testSaltLength(keyLength, hash, hLen) { testFipsSignRejected(vector) : testSign(vector)); }); - if (fips3) { - variations.push(assert.rejects( - subtle.generateKey({ - name: 'RSA-PSS', - modulusLength: 1024, - publicExponent: new Uint8Array([1, 0, 1]), - hash: 'SHA-256', - }, false, ['sign', 'verify']), - (err) => err.name === 'OperationError' && - err.cause?.code === 'ERR_OSSL_RSA_INVALID_MODULUS')); - } - for (const keyLength of fips3 ? [2048] : [1024, 2048]) { for (const [hash, hLen] of [ ['SHA-1', 20],