diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 150f489c541b8c..1d351ab90bc7c4 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1658,7 +1658,11 @@ class SubtleCrypto { } } - return check(operation, algorithm, length); + try { + return check(operation, algorithm, length); + } catch { + return false; + } } } @@ -1701,25 +1705,35 @@ function check(op, alg, length) { return true; case 'deriveBits': { if (normalizedAlgorithm.name === 'HKDF') { - try { - require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); } if (normalizedAlgorithm.name === 'PBKDF2') { - try { - require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); } if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) { - try { - require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); - } catch { + require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); + } + + if (normalizedAlgorithm.name === 'X25519' && length > 256) { + return false; + } + + if (normalizedAlgorithm.name === 'X448' && length > 448) { + return false; + } + + if (normalizedAlgorithm.name === 'ECDH') { + const namedCurve = getCryptoKeyAlgorithm(normalizedAlgorithm.public).namedCurve; + const maxLength = { + '__proto__': null, + 'P-256': 256, + 'P-384': 384, + 'P-521': 528, + }[namedCurve]; + + if (length > maxLength) { return false; } } diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 196f4588188b48..931be98a824032 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -103,18 +103,36 @@ export const vectors = { [true, { name: 'X25519', public: X25519.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'X25519', public: X25519.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [true, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], [true, { name: 'X25519', public: X25519.publicKey }, 'HKDF'], [true, { name: 'ECDH', public: ECDH.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 264 }], [true, { name: 'ECDH', public: ECDH.publicKey }, 'HKDF'], @@ -143,10 +161,18 @@ export const vectors = { [true, { name: 'ECDH', public: ECDH.publicKey }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, 256], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 257], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 264], [false, { name: 'ECDH', public: ECDH.privateKey }], [false, 'ECDH'], [true, { name: 'X25519', public: X25519.publicKey }], + [true, { name: 'X25519', public: X25519.publicKey }, 256], + [false, { name: 'X25519', public: X25519.publicKey }, 257], [false, { name: 'X25519', public: X25519.privateKey }], [false, 'X25519'], ], diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index e2799b5baf40fc..56bb89c28afaae 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -28,15 +28,23 @@ export const vectors = { [!boringSSL, { name: 'X448', public: X448?.publicKey }, { name: 'AES-CBC', length: 128 }], - [!boringSSL, + [false, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 448 }], + [false, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 449 }], [!boringSSL, { name: 'X448', public: X448?.publicKey }, 'HKDF'], ], 'deriveBits': [ [!boringSSL, { name: 'X448', public: X448?.publicKey }], + [!boringSSL, { name: 'X448', public: X448?.publicKey }, 448], + [false, { name: 'X448', public: X448?.publicKey }, 449], [false, { name: 'X448', public: X25519.publicKey }], [false, { name: 'X448', public: X448?.privateKey }], [false, 'X448'], diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 2129ee15c5b8a8..d20bc721cb91ed 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -34,7 +34,7 @@ Last update: - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/65a2134d50/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - web-locks: https://github.com/web-platform-tests/wpt/tree/10a122a6bc/web-locks -- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/2cb332d710/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/8b5cd267b4/WebCryptoAPI - webidl: https://github.com/web-platform-tests/wpt/tree/63ca529a02/webidl - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel diff --git a/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js new file mode 100644 index 00000000000000..709ffcd43e2962 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js @@ -0,0 +1,159 @@ +// META: title=WebCrypto API: supports method tests for algorithms in https://wicg.github.io/webcrypto-modern-algos/ +// META: script=util/helpers.js + +'use strict'; + +const modernAlgorithms = { + // Asymmetric algorithms + 'ML-DSA-44': { + operations: ['generateKey', 'importKey', 'sign', 'verify'], + }, + 'ML-DSA-65': { + operations: ['generateKey', 'importKey', 'sign', 'verify'], + }, + 'ML-DSA-87': { + operations: ['generateKey', 'importKey', 'sign', 'verify'], + }, + 'ML-KEM-512': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits' + ], + }, + 'ML-KEM-768': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits' + ], + }, + 'ML-KEM-1024': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits' + ], + }, + + // Symmetric algorithms + 'ChaCha20-Poly1305': { + operations: ['generateKey', 'importKey', 'encrypt', 'decrypt'], + encryptParams: {name: 'ChaCha20-Poly1305', iv: new Uint8Array(12)}, + }, + +}; + +const operations = [ + 'generateKey', + 'importKey', + 'sign', + 'verify', + 'encrypt', + 'decrypt', + 'deriveBits', + 'digest', + 'encapsulateKey', + 'encapsulateBits', + 'decapsulateKey', + 'decapsulateBits', +]; + +// Test that supports method exists and is a static method +test(() => { + assert_true( + typeof SubtleCrypto.supports === 'function', + 'SubtleCrypto.supports should be a function'); +}, 'SubtleCrypto.supports method exists'); + + +// Test standard WebCrypto algorithms for requested operations +for (const [algorithmName, algorithmInfo] of Object.entries(modernAlgorithms)) { + for (const operation of operations) { + promise_test(async (t) => { + const isSupported = algorithmInfo.operations.includes(operation); + + // Use appropriate algorithm parameters for each operation + let algorithm; + let lengthOrAdditionalAlgorithm; + switch (operation) { + case 'generateKey': + algorithm = algorithmInfo.keyGenParams || algorithmName; + break; + case 'importKey': + algorithm = algorithmInfo.importParams || algorithmName; + break; + case 'sign': + case 'verify': + algorithm = algorithmInfo.signParams || algorithmName; + break; + case 'encrypt': + case 'decrypt': + algorithm = algorithmInfo.encryptParams || algorithmName; + break; + case 'deriveBits': + algorithm = algorithmInfo.deriveBitsParams || algorithmName; + if (algorithm?.public instanceof Promise) { + algorithm.public = (await algorithm.public).publicKey; + } + if (algorithmName === 'PBKDF2' || algorithmName === 'HKDF') { + lengthOrAdditionalAlgorithm = 256; + } + break; + case 'digest': + algorithm = algorithmName; + break; + case 'encapsulateKey': + case 'encapsulateBits': + case 'decapsulateKey': + case 'decapsulateBits': + algorithm = algorithmName; + if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { + lengthOrAdditionalAlgorithm = { name: 'AES-GCM', length: 256 }; + } + break; + default: + algorithm = algorithmName; + } + + const result = SubtleCrypto.supports(operation, algorithm, lengthOrAdditionalAlgorithm); + + if (isSupported) { + assert_true(result, `${algorithmName} should support ${operation}`); + } else { + assert_false( + result, `${algorithmName} should not support ${operation}`); + } + }, `supports(${operation}, ${algorithmName})`); + } +} + +// Test some algorithm objects with valid parameters +test(() => { + assert_true( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with valid tagLength'); +}, 'supports returns true for algorithm objects with valid parameters'); + +// Test some algorithm objects with invalid parameters +test(() => { + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 100, + }), + 'ChaCha20-Poly1305 encrypt with invalid tagLength'); + + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(10), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with invalid iv'); +}, 'supports returns false for algorithm objects with invalid parameters'); + + +done(); diff --git a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js index dd39273e4b918b..921212a0c21dfa 100644 --- a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js @@ -268,6 +268,137 @@ test(() => { }), 'Invalid hash parameter should return false' ); + assert_false( + SubtleCrypto.supports( + 'encrypt', {name: 'AES-CBC', iv: new Uint8Array(10)}), + 'Invalid IV for AES-CBC should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(10), + length: 128, + }), + 'Invalid IV for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 0, + }), + 'Invalid length=0 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 129, + }), + 'Invalid length=129 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-GCM', + iv: new Uint8Array(16), + tagLength: 100, + }), + 'Invalid tag length for AES-GCM should return false'); + assert_false( + SubtleCrypto.supports('generateKey', {name: 'ECDH', namedCurve: 'P-51'}), + 'Invalid curve for ECDH should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-25', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 8), + 'Invalid hash for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 11), + 'Invalid length for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }), + 'null length for HKDF should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-25', + }), + 'Invalid hash for HMAC should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-256', + length: 0, + }), + 'Invalid length for HMAC should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-25', + salt: new Uint8Array(16), + iterations: 100000, + }, + 8), + 'Invalid hash for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }, + 11), + 'Invalid length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }), + 'null length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PKCS1 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PSS should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-26', + }), + 'Invalid hash for RSA OAEP should return false'); + }, 'supports returns false for algorithm objects with invalid parameters'); // Test some specific combinations that should work @@ -394,4 +525,49 @@ test(() => { ); }, 'Invalid algorithm and operation combinations fail'); +// Test supports for deriveKey op +test(() => { + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey HKDF-HMAC should pass'); +}, 'deriveKey tests'); + +promise_test(async (t) => { + let keypair = await crypto.subtle.generateKey( + { + name: 'X25519', + }, + false, ['deriveKey', 'deriveBits']); + + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'AES-GCM', length: 256}), + + 'deriveKey X25519-AES-GCM-256 should pass'); + + assert_false( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey X25519-HMAC-SHA-256 should fail'); +}, 'deriveKey promise tests'); + + + done(); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 68dde063450d82..33b5125e293273 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "2cb332d71030ba0200610d72b94bb1badf447418", + "commit": "8b5cd267b480d75bce41aa306bebbd07ce414fa5", "path": "WebCryptoAPI" }, "webidl": { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 722a0b38398e1d..253877f1a970e0 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -6,16 +6,27 @@ const { hasOpenSSL } = require('../../common/crypto.js'); const s390x = os.arch() === 's390x'; -const conditionalSkips = {}; +const conditionalFileSkips = {}; +const conditionalSubtestSkips = {}; function skip(...files) { for (const file of files) { - conditionalSkips[file] = { - 'skip': `Unsupported in OpenSSL ${process.versions.openssl}`, + conditionalFileSkips[file] = { + 'skip': 'Unsupported in ' + (process.features.openssl_is_boringssl ? 'BoringSSL' : `OpenSSL ${process.versions.openssl}`), }; } } +function skipSubtests(...entries) { + for (const [file, regexp] of entries) { + conditionalSubtestSkips[file] ||= { + 'skipTests': [], + }; + + conditionalSubtestSkips[file].skipTests.push(regexp); + } +} + if (!hasOpenSSL(3, 0)) { skip( 'encrypt_decrypt/aes_ocb.tentative.https.any.js', @@ -45,10 +56,25 @@ if (!hasOpenSSL(3, 5)) { 'import_export/ML-DSA_importKey.tentative.https.any.js', 'import_export/ML-KEM_importKey.tentative.https.any.js', 'sign_verify/mldsa.tentative.https.any.js'); + + skipSubtests( + ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); } +function assertNoOverlap(fileSkips, subtestSkips) { + const subtestSkipFiles = new Set(Object.keys(subtestSkips)); + const overlap = Object.keys(fileSkips).filter((file) => subtestSkipFiles.has(file)); + + if (overlap.length !== 0) { + throw new Error(`conditionalFileSkips and conditionalSubtestSkips overlap: ${overlap.join(', ')}`); + } +} + +assertNoOverlap(conditionalFileSkips, conditionalSubtestSkips); + module.exports = { - ...conditionalSkips, + ...conditionalFileSkips, + ...conditionalSubtestSkips, 'algorithm-discards-context.https.window.js': { 'skip': 'Not relevant in Node.js context', },