From a5a37be195237cebf826e054370aab1f2d252c1e Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 25 Aug 2026 22:51:29 +0200 Subject: [PATCH 1/3] crypto: fix RSA-PSS oversized salt handling Let OpenSSL handle representable salt lengths so verification of an impossible length resolves false. Guard values outside the native int32 parameter range to prevent SignJob from silently ignoring them. Move the digest-size helper next to HKDF, its remaining consumer. Signed-off-by: Filip Skokan --- lib/internal/crypto/hkdf.js | 17 ++++++++- lib/internal/crypto/rsa.js | 27 ++++++-------- lib/internal/crypto/util.js | 17 --------- .../test-webcrypto-sign-verify-rsa.js | 36 ++++++++++--------- 4 files changed, 47 insertions(+), 50 deletions(-) diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index d55968907541..fcff7070164f 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -22,7 +22,6 @@ const { const { kMaxLength } = require('buffer'); const { - getDigestSizeInBytes, jobPromise, normalizeHashName, toBuf, @@ -142,6 +141,22 @@ function hkdfSync(hash, key, salt, info, length) { return bits; } +function getDigestSizeInBytes(name) { + switch (name) { + case 'SHA-1': + return 20; + case 'SHA-256': // Fall through + case 'SHA3-256': + return 32; + case 'SHA-384': // Fall through + case 'SHA3-384': + return 48; + case 'SHA-512': // Fall through + case 'SHA3-512': + return 64; + } +} + function validateHkdfDeriveBitsLength(length, hash) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 846b6b7bf748..6de51f6ecf6e 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -1,7 +1,6 @@ 'use strict'; const { - MathCeil, TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -24,12 +23,11 @@ const { } = internalBinding('crypto'); const { - validateInt32, + isInt32, } = require('internal/validators'); const { bigIntArrayToUnsignedInt, - getDigestSizeInBytes, getUsagesMask, jobPromise, normalizeHashName, @@ -241,19 +239,16 @@ function rsaSignVerify(key, data, { saltLength }, signature) { throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); const algorithm = getCryptoKeyAlgorithm(key); - if (algorithm.name === 'RSA-PSS') { - try { - validateInt32( - saltLength, - 'algorithm.saltLength', - 0, - MathCeil((algorithm.modulusLength - 1) / 8) - - getDigestSizeInBytes(algorithm.hash.name) - 2); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + // RsaPssParams converts saltLength to an unsigned long, but SignJob only + // accepts int32 values. + if (algorithm.name === 'RSA-PSS' && !isInt32(saltLength)) { + // EMSA-PSS-VERIFY treats an impossible salt length as inconsistent. + if (mode === kSignJobModeVerify) + return false; + + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError'); } return jobPromise(() => new SignJob( diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 3c52feebea68..5c0fd334eabd 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -1042,22 +1042,6 @@ function getBlockSize(name) { } } -function getDigestSizeInBytes(name) { - switch (name) { - case 'SHA-1': - return 20; - case 'SHA-256': // Fall through - case 'SHA3-256': - return 32; - case 'SHA-384': // Fall through - case 'SHA3-384': - return 48; - case 'SHA-512': // Fall through - case 'SHA3-512': - return 64; - } -} - function validateKeyOps(keyOps, usagesSet) { if (keyOps === undefined) return; validateArray(keyOps, 'keyData.key_ops'); @@ -1132,7 +1116,6 @@ module.exports = { bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, getBlockSize, - getDigestSizeInBytes, getStringOption, getUsagesMask, getUsagesFromMask, diff --git a/test/parallel/test-webcrypto-sign-verify-rsa.js b/test/parallel/test-webcrypto-sign-verify-rsa.js index 62e9cbe7826a..12633cd50de2 100644 --- a/test/parallel/test-webcrypto-sign-verify-rsa.js +++ b/test/parallel/test-webcrypto-sign-verify-rsa.js @@ -227,22 +227,26 @@ async function testSaltLength(keyLength, hash, hLen) { const signature = await subtle.sign( { name: 'RSA-PSS', saltLength: max }, privateKey, data); - await assert.rejects( - subtle.sign({ name: 'RSA-PSS', saltLength: max + 1 }, privateKey, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`); - return true; - }); - await subtle.verify( - { name: 'RSA-PSS', saltLength: max }, publicKey, signature, data); - await assert.rejects( - subtle.verify({ name: 'RSA-PSS', saltLength: max + 1 }, publicKey, signature, data), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause?.code, 'ERR_OUT_OF_RANGE'); - assert.strictEqual(err.cause?.message, `The value of "algorithm.saltLength" is out of range. It must be >= 0 && <= ${max}. Received ${max + 1}`); - return true; - }); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength: max }, publicKey, signature, data), true); + + for (const saltLength of [max + 1, 0x7fffffff]) { + await assert.rejects( + subtle.sign({ name: 'RSA-PSS', saltLength }, privateKey, data), { + name: 'OperationError', + }); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength }, publicKey, signature, data), false); + } + + for (const saltLength of [0x80000000, 0xffffffff]) { + await assert.rejects( + subtle.sign({ name: 'RSA-PSS', saltLength }, privateKey, data), { + name: 'OperationError', + }); + assert.strictEqual(await subtle.verify( + { name: 'RSA-PSS', saltLength }, publicKey, signature, data), false); + } } (async function() { From 1a201cdc6a1ecdc01b05fee4fb94a5e08debad16 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 25 Aug 2026 22:52:07 +0200 Subject: [PATCH 2/3] crypto: fix private SPKI export error Reject SPKI export of a private asymmetric key with InvalidAccessError as required by the algorithm export steps. Let wrapKey propagate the same export failure. Signed-off-by: Filip Skokan --- lib/internal/crypto/webcrypto.js | 30 +++++++++++-------- .../test-webcrypto-export-import-ec.js | 6 ++++ test/parallel/test-webcrypto-wrap-unwrap.js | 8 ++--- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 55953d8e4745..342b5ee05fe0 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -482,19 +482,20 @@ function deriveKeyImpl( } function exportKeySpki(key) { + let exporter; switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return require('internal/crypto/rsa') - .rsaExportKey(key, kWebCryptoKeyFormatSPKI); + exporter = require('internal/crypto/rsa').rsaExportKey; + break; case 'ECDSA': // Fall through case 'ECDH': - return require('internal/crypto/ec') - .ecExportKey(key, kWebCryptoKeyFormatSPKI); + exporter = require('internal/crypto/ec').ecExportKey; + break; case 'Ed25519': // Fall through case 'Ed448': @@ -502,25 +503,30 @@ function exportKeySpki(key) { case 'X25519': // Fall through case 'X448': - return require('internal/crypto/cfrg') - .cfrgExportKey(key, kWebCryptoKeyFormatSPKI); + exporter = require('internal/crypto/cfrg').cfrgExportKey; + break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return require('internal/crypto/ml_dsa') - .mlDsaExportKey(key, kWebCryptoKeyFormatSPKI); + exporter = require('internal/crypto/ml_dsa').mlDsaExportKey; + break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - return require('internal/crypto/ml_kem') - .mlKemExportKey(key, kWebCryptoKeyFormatSPKI); + exporter = require('internal/crypto/ml_kem').mlKemExportKey; + break; default: return undefined; } + + if (getCryptoKeyType(key) !== 'public') + throw lazyDOMException('Key must be a public key', 'InvalidAccessError'); + + return exporter(key, kWebCryptoKeyFormatSPKI); } function exportKeyPkcs8(key) { @@ -767,9 +773,7 @@ function exportKeySync(format, key) { let result; switch (format) { case 'spki': { - if (type === 'public') { - result = exportKeySpki(key); - } + result = exportKeySpki(key); break; } case 'pkcs8': { diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index f6157bff2b69..7509211285d1 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -163,6 +163,12 @@ async function testImportPkcs8( assert.strictEqual( Buffer.from(pkcs8).toString('hex'), keyData[namedCurve].pkcs8.toString('hex')); + + await assert.rejects( + subtle.exportKey('spki', key), { + message: 'Key must be a public key', + name: 'InvalidAccessError', + }); } else { await assert.rejects( subtle.exportKey('pkcs8', key), { diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index c2c089ed0ffa..64ff4c5f5342 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -506,7 +506,7 @@ async function testNonByteLengthWrapUnwrap({ // Test that wrapKey/unwrapKey validate the wrapping/unwrapping key's // algorithm and usage before proceeding. // Spec: https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey -// Steps 9-10 (wrapping key checks) must precede step 12 (exportKey). +// Steps 9-10 (wrapping key checks) must precede step 13 (export operation). (async function() { const hmacKey = await subtle.generateKey( { name: 'HMAC', hash: 'SHA-256' }, @@ -551,7 +551,7 @@ async function testNonByteLengthWrapUnwrap({ }); // Correct wrapping key algorithm and usage results in the expected - // exportKey error (not the wrapping key validation error). + // export operation error (not the wrapping key validation error). const wrapKey = await subtle.generateKey( { name: 'AES-GCM', length: 128 }, true, @@ -563,8 +563,8 @@ async function testNonByteLengthWrapUnwrap({ name: 'AES-GCM', iv: new Uint8Array(12), }), { - // exportKey('spki', privateKey) throws NotSupportedError - name: 'NotSupportedError', + message: 'Key must be a public key', + name: 'InvalidAccessError', }); // --- unwrapKey validation tests --- From be762561eb2e6a1da5342a9d61b312a827ae60c5 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 25 Aug 2026 22:52:35 +0200 Subject: [PATCH 3/3] crypto: validate JWK usages before key_ops Check requested usages against the JWK public or private key type before validating key_ops. This preserves the SyntaxError precedence specified for RSA, EC, CFRG, ML-DSA, and ML-KEM imports. Signed-off-by: Filip Skokan --- lib/internal/crypto/cfrg.js | 11 +++--- lib/internal/crypto/ec.js | 11 +++--- lib/internal/crypto/ml_dsa.js | 11 +++--- lib/internal/crypto/ml_kem.js | 11 +++--- lib/internal/crypto/rsa.js | 11 +++--- .../test-webcrypto-export-import-cfrg.js | 35 ++++++++++++++++++ .../test-webcrypto-export-import-ec.js | 36 +++++++++++++++++++ .../test-webcrypto-export-import-ml-dsa.js | 34 ++++++++++++++++++ .../test-webcrypto-export-import-ml-kem.js | 28 +++++++++++---- .../test-webcrypto-export-import-rsa.js | 29 +++++++++++++++ 10 files changed, 186 insertions(+), 31 deletions(-) diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 9eea26aa36a0..1f7b3202fb10 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -146,6 +146,12 @@ function cfrgImportKey( break; } case 'jwk': { + const isPublic = keyData.d === undefined; + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); + const expectedUse = (name === 'X25519' || name === 'X448') ? 'enc' : 'sig'; validateJwk(keyData, 'OKP', extractable, usagesSet, expectedUse); @@ -159,11 +165,6 @@ function cfrgImportKey( 'JWK "alg" does not match the requested algorithm', 'DataError'); } - const isPublic = keyData.d === undefined; - verifyAcceptableKeyUse( - name, - usagesSet, - isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index cbd89dd11c7e..0f1e0555202d 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -164,6 +164,12 @@ function ecImportKey( break; } case 'jwk': { + const isPublic = keyData.d === undefined; + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); + const expectedUse = name === 'ECDH' ? 'enc' : 'sig'; validateJwk(keyData, 'EC', extractable, usagesSet, expectedUse); @@ -185,11 +191,6 @@ function ecImportKey( 'DataError'); } - const isPublic = keyData.d === undefined; - verifyAcceptableKeyUse( - name, - usagesSet, - isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 0756198312c0..108ec22fc791 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -158,17 +158,18 @@ function mlDsaImportKey( break; } case 'jwk': { + const isPublic = keyData.priv === undefined; + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); + validateJwk(keyData, 'AKP', extractable, usagesSet, 'sig'); if (keyData.alg !== name) throw lazyDOMException( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); - const isPublic = keyData.priv === undefined; - verifyAcceptableKeyUse( - name, - usagesSet, - isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index c917c88c0f29..da077ac8344d 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -169,17 +169,18 @@ function mlKemImportKey( break; } case 'jwk': { + const isPublic = keyData.priv === undefined; + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); + validateJwk(keyData, 'AKP', extractable, usagesSet, 'enc'); if (keyData.alg !== name) throw lazyDOMException( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); - const isPublic = keyData.priv === undefined; - verifyAcceptableKeyUse( - name, - usagesSet, - isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 6de51f6ecf6e..27b23087d563 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -186,6 +186,12 @@ function rsaImportKey( break; } case 'jwk': { + const isPublic = keyData.d === undefined; + verifyAcceptableKeyUse( + algorithm.name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); + const expectedUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig'; validateJwk(keyData, 'RSA', extractable, usagesSet, expectedUse); @@ -202,11 +208,6 @@ function rsaImportKey( 'DataError'); } - const isPublic = keyData.d === undefined; - verifyAcceptableKeyUse( - algorithm.name, - usagesSet, - isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index 0ff7c61bdc68..fb8014840f34 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -432,6 +432,41 @@ async function testImportRaw({ name, publicUsages }) { await Promise.all(tests); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + for (const { name, publicUsages, privateUsages } of testVectors) { + const jwk = keyData[name].jwk; + const publicJwk = { + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x, + }; + const isKeyAgreement = name.startsWith('X'); + const invalidUsage = isKeyAgreement ? + privateUsages[0] : publicUsages[0]; + const invalidJwk = isKeyAgreement ? publicJwk : jwk; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...invalidJwk, key_ops: [invalidUsage, invalidUsage] }, + { name }, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + const validUsage = privateUsages[0]; + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [validUsage, validUsage] }, + { name }, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + { const rsaPublic = crypto.createPublicKey( fixtures.readKey('rsa_public_2048.pem')); diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index 7509211285d1..978aad14ebd9 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -412,6 +412,42 @@ async function testImportRaw({ name, publicUsages }, namedCurve) { await Promise.all(tests); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + const jwk = keyData['P-256'].jwk; + const publicJwk = { + kty: jwk.kty, + crv: jwk.crv, + x: jwk.x, + y: jwk.y, + }; + + for (const { name, publicUsages, privateUsages } of testVectors) { + const invalidUsage = name === 'ECDH' ? + privateUsages[0] : publicUsages[0]; + const invalidJwk = name === 'ECDH' ? publicJwk : jwk; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...invalidJwk, key_ops: [invalidUsage, invalidUsage] }, + { name, namedCurve: 'P-256' }, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + const validUsage = privateUsages[0]; + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [validUsage, validUsage] }, + { name, namedCurve: 'P-256' }, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + // https://github.com/nodejs/node/issues/45859 (async function() { diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index 20d46870e430..9c6e04da053c 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -491,6 +491,40 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { }); })().then(common.mustCall()); +// JWK key usage validation precedes `key_ops` validation. +(async function() { + const privateJwk = keyData['ML-DSA-65'].jwk; + const publicJwk = { ...privateJwk, priv: undefined }; + + for (const [jwk, usage] of [ + [privateJwk, 'verify'], + [publicJwk, 'sign'], + ]) { + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [usage, usage] }, + 'ML-DSA-65', + true, + [usage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + } + + for (const [jwk, usage] of [ + [privateJwk, 'sign'], + [publicJwk, 'verify'], + ]) { + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, key_ops: [usage, usage] }, + 'ML-DSA-65', + true, + [usage]), + { name: 'DataError', message: /Duplicate key operation/ }); + } +})().then(common.mustCall()); + if (!process.features.openssl_is_boringssl) { (async function() { for (const { name, privateUsages } of testVectors) { diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index cd05224969c8..862eea96726d 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -496,13 +496,29 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) }); })().then(common.mustCall()); -// Regression test: JWK `key_ops` validation must recognize ML-KEM operations -// (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that -// duplicate entries are rejected +// JWK key usage validation precedes `key_ops` validation. (async function() { - for (const op of ['encapsulateKey', 'encapsulateBits', - 'decapsulateKey', 'decapsulateBits']) { - const jwk = { ...keyData['ML-KEM-768'].jwk, key_ops: [op, op] }; + const privateJwk = keyData['ML-KEM-768'].jwk; + const encapsulationOps = ['encapsulateKey', 'encapsulateBits']; + const decapsulationOps = ['decapsulateKey', 'decapsulateBits']; + + for (const op of encapsulationOps) { + const jwk = { ...privateJwk, key_ops: [op, op] }; + await assert.rejects( + subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + } + + // Duplicate entries are still rejected when the requested usages are valid. + for (const op of encapsulationOps) { + const jwk = { ...privateJwk, priv: undefined, key_ops: [op, op] }; + await assert.rejects( + subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), + { name: 'DataError', message: /Duplicate key operation/ }); + } + + for (const op of decapsulationOps) { + const jwk = { ...privateJwk, key_ops: [op, op] }; await assert.rejects( subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), { name: 'DataError', message: /Duplicate key operation/ }); diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index 8808fcbe6b1f..294baff544a6 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -651,6 +651,35 @@ const testVectors = [ await Promise.all(variations); })().then(common.mustCall()); +// Type-specific JWK usage validation precedes `key_ops` validation. +(async function() { + const privateJwk = keyData[1024].jwk; + + for (const { name, publicUsages, privateUsages } of testVectors) { + const algorithm = { name, hash: 'SHA-256' }; + const invalidUsage = publicUsages[0]; + const validUsage = privateUsages[0]; + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...privateJwk, key_ops: [invalidUsage, invalidUsage] }, + algorithm, + true, + [invalidUsage]), + { name: 'SyntaxError', message: /Unsupported key usage/ }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...privateJwk, key_ops: [validUsage, validUsage] }, + algorithm, + true, + [validUsage]), + { name: 'DataError', message: 'Duplicate key operation' }); + } +})().then(common.mustCall()); + { const ecPublic = crypto.createPublicKey( fixtures.readKey('ec_p256_public.pem'));