diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index cc443b575da46a..0e6189b6df832c 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -1,7 +1,6 @@ 'use strict'; const { - ArrayFrom, ArrayPrototypePush, SafeSet, } = primordials; @@ -210,7 +209,7 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) { return new InternalCryptoKey( key, { name, length }, - ArrayFrom(usagesSet), + usagesSet, extractable); } @@ -285,7 +284,7 @@ function aesImportKey( return new InternalCryptoKey( keyObject, { name, length }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 58e8fb02943b78..039f5d8b6635e7 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -134,7 +134,7 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { case 'X25519': // Fall through case 'X448': - publicUsages = []; + publicUsages = new SafeSet(); privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); break; } @@ -245,7 +245,7 @@ function cfrgImportKey( return new InternalCryptoKey( keyObject, { name }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 2230097c4c5c9f..fe2ba389d52c66 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -1,7 +1,6 @@ 'use strict'; const { - ArrayFrom, SafeSet, } = primordials; @@ -81,7 +80,7 @@ async function c20pGenerateKey(algorithm, extractable, keyUsages) { return new InternalCryptoKey( createSecretKey(keyData), { name }, - ArrayFrom(usagesSet), + usagesSet, extractable); } @@ -140,7 +139,7 @@ function c20pImportKey( return new InternalCryptoKey( keyObject, { name }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 3b0b35e8f0f822..e9c72bc58918e8 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -119,7 +119,7 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { privateUsages = getUsagesUnion(usageSet, 'sign'); break; case 'ECDH': - publicUsages = []; + publicUsages = new SafeSet(); privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); break; } @@ -271,7 +271,7 @@ function ecImportKey( return new InternalCryptoKey( keyObject, { name, namedCurve }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index a1acb782a5f5db..595b9c2b1e849a 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -61,6 +61,7 @@ const { bigIntArrayToUnsignedBigInt, normalizeAlgorithm, hasAnyNotIn, + getSortedUsages, } = require('internal/crypto/util'); const { @@ -901,7 +902,7 @@ class InternalCryptoKey { keyObject, algorithm, extractable, - keyUsages, + getSortedUsages(new SafeSet(keyUsages)), ); } } @@ -967,7 +968,7 @@ function importGenericSecretKey( return undefined; } - return new InternalCryptoKey(keyObject, { name }, keyUsages, false); + return new InternalCryptoKey(keyObject, { name }, usagesSet, false); } module.exports = { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 1689d19d4a0660..4331f95355ba23 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -1,7 +1,6 @@ 'use strict'; const { - ArrayFrom, SafeSet, StringPrototypeSubstring, } = primordials; @@ -81,7 +80,7 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) { return new InternalCryptoKey( key, { name, length, hash }, - ArrayFrom(usageSet), + usageSet, extractable); } @@ -115,7 +114,7 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) { return new InternalCryptoKey( createSecretKey(keyData), { name, length }, - ArrayFrom(usageSet), + usageSet, extractable); } @@ -196,7 +195,7 @@ function macImportKey( return new InternalCryptoKey( keyObject, algorithmObject, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index e6c70db034275f..283c695686a69a 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -203,7 +203,7 @@ function mlDsaImportKey( return new InternalCryptoKey( keyObject, { name }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 6c665c20c0c583..4f1207a2c27fc2 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -69,8 +69,8 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { { name: 'OperationError', cause: err }); } - const publicUsages = getUsagesUnion(usageSet, 'encapsulateBits', 'encapsulateKey'); - const privateUsages = getUsagesUnion(usageSet, 'decapsulateBits', 'decapsulateKey'); + const publicUsages = getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); + const privateUsages = getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); const keyAlgorithm = { name }; @@ -202,7 +202,7 @@ function mlKemImportKey( return new InternalCryptoKey( keyObject, { name }, - keyUsages, + usagesSet, extractable); } diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index b87ef49dbd5aa4..f6f1f09a294131 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -281,7 +281,7 @@ function rsaImportKey( modulusLength, publicExponent: new Uint8Array(publicExponent), hash: algorithm.hash, - }, keyUsages, extractable); + }, usagesSet, extractable); } async function rsaSignVerify(key, data, { saltLength }, signature) { diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index 70e1027946190a..2f9bafc39ac7c4 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -3,6 +3,7 @@ const { ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, + ArrayFrom, ArrayPrototypeIncludes, ArrayPrototypePush, BigInt, @@ -16,6 +17,7 @@ const { ObjectKeys, ObjectPrototypeHasOwnProperty, PromiseWithResolvers, + SafeSet, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -718,14 +720,40 @@ function getStringOption(options, key) { } function getUsagesUnion(usageSet, ...usages) { - const newset = []; + const newset = new SafeSet(); for (let n = 0; n < usages.length; n++) { if (usageSet.has(usages[n])) - ArrayPrototypePush(newset, usages[n]); + newset.add(usages[n]); } return newset; } +const kCanonicalUsageOrder = new SafeSet([ + 'encrypt', 'decrypt', + 'sign', 'verify', + 'deriveKey', 'deriveBits', + 'wrapKey', 'unwrapKey', + 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', +]); + +/** + * Returns the usages from `usageSet` as an array in the canonical order + * defined by {@link kCanonicalUsageOrder}. + * @param {SafeSet} usageSet + * @returns {string[]} + */ +function getSortedUsages(usageSet) { + if (usageSet.size <= 1) { + return ArrayFrom(usageSet); + } + const result = []; + for (const usage of kCanonicalUsageOrder) { + if (usageSet.has(usage)) ArrayPrototypePush(result, usage); + } + return result; +} + function getBlockSize(name) { switch (name) { case 'SHA-1': @@ -841,6 +869,7 @@ module.exports = { getDigestSizeInBytes, getStringOption, getUsagesUnion, + getSortedUsages, secureHeapUsed, getCachedHashId, getHashCache, diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js new file mode 100644 index 00000000000000..e30dbe7887166e --- /dev/null +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -0,0 +1,568 @@ +'use strict'; + +// Regression test for https://github.com/nodejs/node/issues/62899 +// SubtleCrypto.generateKey(), SubtleCrypto.importKey(), and +// KeyObject.prototype.toCryptoKey() should produce CryptoKey values whose +// `usages` have been de-duplicated and returned in a canonical order. +// The same applies to `key_ops` on JWK exports of extractable keys. + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { createSecretKey } = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); +const { subtle } = globalThis.crypto; + +function assertSameSet(actual, expected, msg) { + if (msg === undefined) { + assert.deepStrictEqual(actual, expected); + } else { + assert.deepStrictEqual(actual, expected, msg); + } +} + +{ + const tests = []; + + // Symmetric keys (single CryptoKey result). Inputs are deliberately in + // non-canonical order so the test exercises the canonical re-ordering. + const symmetric = [ + { algorithm: { name: 'HMAC', hash: 'SHA-256' }, + usages: ['verify', 'sign', 'verify', 'sign'], + expected: ['sign', 'verify'] }, + { algorithm: { name: 'AES-CTR', length: 128 }, + usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, + { algorithm: { name: 'AES-CBC', length: 128 }, + usages: ['encrypt', 'encrypt'], + expected: ['encrypt'] }, + { algorithm: { name: 'AES-GCM', length: 128 }, + usages: ['decrypt', 'encrypt', 'decrypt'], + expected: ['encrypt', 'decrypt'] }, + ]; + + if (!process.features.openssl_is_boringssl) { + symmetric.push({ + algorithm: { name: 'AES-KW', length: 128 }, + usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], + expected: ['wrapKey', 'unwrapKey'], + }); + } else { + common.printSkipMessage('AES-KW is not supported in BoringSSL'); + } + + if (hasOpenSSL(3)) { + symmetric.push({ + algorithm: { name: 'AES-OCB', length: 128 }, + usages: ['decrypt', 'encrypt', 'decrypt', 'encrypt'], + expected: ['encrypt', 'decrypt'], + }); + symmetric.push({ + algorithm: { name: 'KMAC128', length: 128 }, + usages: ['verify', 'sign', 'verify', 'sign'], + expected: ['sign', 'verify'], + }); + } else { + common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3'); + } + + if (!process.features.openssl_is_boringssl) { + symmetric.push({ + algorithm: { name: 'ChaCha20-Poly1305' }, + usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'], + }); + } else { + common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); + } + + for (const { algorithm, usages, expected } of symmetric) { + tests.push((async () => { + const key = await subtle.generateKey(algorithm, true, usages); + assertSameSet(key.usages, expected, + `generateKey ${algorithm.name}`); + assert.strictEqual(key.usages.length, expected.length, + `generateKey ${algorithm.name} usage count`); + })()); + } + + // Asymmetric keys (CryptoKeyPair result). Duplicates across the input + // must not produce duplicates on either the public or private key. + const asymmetric = [ + { algorithm: { name: 'RSA-OAEP', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + usages: ['wrapKey', 'unwrapKey', 'decrypt', 'encrypt', + 'unwrapKey', 'wrapKey', 'decrypt', 'encrypt'], + publicExpected: ['encrypt', 'wrapKey'], + privateExpected: ['decrypt', 'unwrapKey'] }, + { algorithm: { name: 'RSA-PSS', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'ECDSA', namedCurve: 'P-256' }, + usages: ['verify', 'sign', 'verify', 'sign', 'verify'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'ECDH', namedCurve: 'P-256' }, + usages: ['deriveBits', 'deriveKey', 'deriveBits', 'deriveKey'], + publicExpected: [], + privateExpected: ['deriveKey', 'deriveBits'] }, + { algorithm: { name: 'Ed25519' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'X25519' }, + usages: ['deriveBits', 'deriveKey', 'deriveBits', 'deriveKey'], + publicExpected: [], + privateExpected: ['deriveKey', 'deriveBits'] }, + ]; + + if (hasOpenSSL(3, 5)) { + asymmetric.push({ + algorithm: { name: 'ML-DSA-65' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'], + }); + asymmetric.push({ + algorithm: { name: 'ML-KEM-768' }, + usages: ['decapsulateBits', 'encapsulateBits', 'decapsulateKey', + 'encapsulateKey', 'decapsulateBits', 'encapsulateBits'], + publicExpected: ['encapsulateKey', 'encapsulateBits'], + privateExpected: ['decapsulateKey', 'decapsulateBits'], + }); + } else { + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + } + + for (const { algorithm, usages, publicExpected, privateExpected } of asymmetric) { + tests.push((async () => { + const { publicKey, privateKey } = + await subtle.generateKey(algorithm, true, usages); + assertSameSet(publicKey.usages, publicExpected, + `generateKey ${algorithm.name} publicKey`); + assert.strictEqual(publicKey.usages.length, publicExpected.length); + assertSameSet(privateKey.usages, privateExpected, + `generateKey ${algorithm.name} privateKey`); + assert.strictEqual(privateKey.usages.length, privateExpected.length); + })()); + } + + Promise.all(tests).then(common.mustCall()); +} + +{ + const tests = []; + + // Symmetric raw imports. + const rawSymmetric = [ + { algorithm: { name: 'AES-CBC' }, keyData: new Uint8Array(16), + usages: ['decrypt', 'encrypt', 'decrypt', 'encrypt'], + expected: ['encrypt', 'decrypt'] }, + { algorithm: { name: 'AES-CTR' }, keyData: new Uint8Array(16), + usages: ['wrapKey', 'encrypt', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'wrapKey'] }, + { algorithm: { name: 'AES-GCM' }, keyData: new Uint8Array(16), + usages: ['decrypt', 'encrypt', 'decrypt'], + expected: ['encrypt', 'decrypt'] }, + { algorithm: { name: 'HMAC', hash: 'SHA-256' }, keyData: new Uint8Array(32), + usages: ['verify', 'sign', 'verify', 'sign'], + expected: ['sign', 'verify'] }, + ]; + + if (!process.features.openssl_is_boringssl) { + rawSymmetric.push({ + algorithm: { name: 'AES-KW' }, keyData: new Uint8Array(16), + usages: ['wrapKey', 'unwrapKey', 'wrapKey'], + expected: ['wrapKey', 'unwrapKey'], + }); + } else { + common.printSkipMessage('AES-KW is not supported in BoringSSL'); + } + + if (hasOpenSSL(3)) { + // KMAC does not support `raw` format, only `raw-secret` and `jwk`. + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', new Uint8Array(16), { name: 'KMAC128' }, true, + ['verify', 'sign', 'verify', 'sign']); + assertSameSet(key.usages, ['sign', 'verify'], + 'importKey raw-secret KMAC128'); + assert.strictEqual(key.usages.length, 2); + })()); + + tests.push((async () => { + const jwk = { + kty: 'oct', + k: Buffer.from(new Uint8Array(16)).toString('base64url'), + alg: 'K128', + }; + const key = await subtle.importKey( + 'jwk', jwk, { name: 'KMAC128' }, true, + ['verify', 'sign', 'verify', 'sign']); + assertSameSet(key.usages, ['sign', 'verify'], + 'importKey jwk KMAC128'); + assert.strictEqual(key.usages.length, 2); + })()); + } else { + common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3'); + } + + for (const { algorithm, keyData, usages, expected } of rawSymmetric) { + tests.push((async () => { + const key = await subtle.importKey('raw', keyData, algorithm, true, usages); + assertSameSet(key.usages, expected, + `importKey raw ${algorithm.name}`); + assert.strictEqual(key.usages.length, expected.length); + })()); + } + + // Generic secret keys (HKDF, PBKDF2) - importGenericSecretKey path. + // These are not extractable. + for (const name of ['HKDF', 'PBKDF2']) { + tests.push((async () => { + const key = await subtle.importKey( + 'raw', + new Uint8Array(16), + name, + false, + ['deriveBits', 'deriveKey', 'deriveBits', 'deriveKey']); + assertSameSet(key.usages, ['deriveKey', 'deriveBits'], + `importKey raw ${name}`); + assert.strictEqual(key.usages.length, 2); + })()); + } + + // Argon2 - also via importGenericSecretKey, deriveBits-only. + // Argon2 only supports raw-secret import. + if (hasOpenSSL(3, 2)) { + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(16), + 'Argon2id', + false, + ['deriveBits', 'deriveBits']); + assertSameSet(key.usages, ['deriveBits'], + 'importKey raw-secret Argon2id'); + assert.strictEqual(key.usages.length, 1); + })()); + } else { + common.printSkipMessage('Argon2 requires OpenSSL >= 3.2'); + } + + // JWK symmetric import. + tests.push((async () => { + const jwk = { + kty: 'oct', + k: 'AAAAAAAAAAAAAAAAAAAAAA', + alg: 'A128CBC', + }; + const key = await subtle.importKey('jwk', jwk, { name: 'AES-CBC' }, true, + ['decrypt', 'encrypt', 'decrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt'], + 'importKey jwk AES-CBC'); + assert.strictEqual(key.usages.length, 2); + })()); + + // Asymmetric import via JWK - RSA, ECDSA, Ed25519. + tests.push((async () => { + // Generate, export, re-import with duplicate usages. + const { privateKey } = await subtle.generateKey( + { name: 'RSA-PSS', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, ['sign', 'verify']); + const jwk = await subtle.exportKey('jwk', privateKey); + const imported = await subtle.importKey( + 'jwk', jwk, + { name: 'RSA-PSS', hash: 'SHA-256' }, + true, + ['sign', 'sign', 'sign']); + assert.deepStrictEqual(imported.usages, ['sign']); + })()); + + tests.push((async () => { + const { privateKey } = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, ['sign', 'verify']); + const jwk = await subtle.exportKey('jwk', privateKey); + const imported = await subtle.importKey( + 'jwk', jwk, + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'sign']); + assert.deepStrictEqual(imported.usages, ['sign']); + })()); + + tests.push((async () => { + const { privateKey } = await subtle.generateKey( + { name: 'Ed25519' }, true, ['sign', 'verify']); + const pkcs8 = await subtle.exportKey('pkcs8', privateKey); + const imported = await subtle.importKey( + 'pkcs8', pkcs8, + { name: 'Ed25519' }, + true, + ['sign', 'sign', 'sign']); + assert.deepStrictEqual(imported.usages, ['sign']); + })()); + + if (hasOpenSSL(3, 5)) { + // ML-DSA JWK roundtrip. + tests.push((async () => { + const { privateKey } = await subtle.generateKey( + { name: 'ML-DSA-65' }, true, ['sign', 'verify']); + const jwk = await subtle.exportKey('jwk', privateKey); + const imported = await subtle.importKey( + 'jwk', jwk, { name: 'ML-DSA-65' }, true, + ['sign', 'sign', 'sign']); + assert.deepStrictEqual(imported.usages, ['sign']); + })()); + + // ML-KEM JWK roundtrip. + tests.push((async () => { + const { privateKey } = await subtle.generateKey( + { name: 'ML-KEM-768' }, true, + ['decapsulateKey', 'decapsulateBits']); + const jwk = await subtle.exportKey('jwk', privateKey); + const imported = await subtle.importKey( + 'jwk', jwk, { name: 'ML-KEM-768' }, true, + ['decapsulateBits', 'decapsulateKey', + 'decapsulateBits', 'decapsulateKey']); + assert.deepStrictEqual(imported.usages, + ['decapsulateKey', 'decapsulateBits']); + })()); + } else { + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + } + + // Spki import of RSA public key. + tests.push((async () => { + const { publicKey } = await subtle.generateKey( + { name: 'RSA-OAEP', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + const spki = await subtle.exportKey('spki', publicKey); + const imported = await subtle.importKey( + 'spki', spki, + { name: 'RSA-OAEP', hash: 'SHA-256' }, + true, + ['wrapKey', 'encrypt', 'wrapKey', 'encrypt']); + assertSameSet(imported.usages, ['encrypt', 'wrapKey']); + assert.strictEqual(imported.usages.length, 2); + })()); + + // ChaCha20-Poly1305 raw-secret import. + if (!process.features.openssl_is_boringssl) { + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(32), + { name: 'ChaCha20-Poly1305' }, + true, + ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt']); + assert.strictEqual(key.usages.length, 2); + })()); + } else { + common.printSkipMessage('ChaCha20-Poly1305 is not supported in BoringSSL'); + } + + // AES-OCB raw-secret import. + if (hasOpenSSL(3)) { + tests.push((async () => { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(16), + { name: 'AES-OCB' }, + true, + ['decrypt', 'encrypt', 'decrypt', 'encrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt']); + assert.strictEqual(key.usages.length, 2); + })()); + } else { + common.printSkipMessage('AES-OCB requires OpenSSL >= 3'); + } + + Promise.all(tests).then(common.mustCall()); +} + +{ + const tests = []; + + // Symmetric: HMAC, AES-*, HKDF, PBKDF2 + tests.push((async () => { + const keyObject = createSecretKey(new Uint8Array(32)); + const key = keyObject.toCryptoKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['verify', 'sign', 'verify', 'sign']); + assertSameSet(key.usages, ['sign', 'verify']); + assert.strictEqual(key.usages.length, 2); + })()); + + tests.push((async () => { + const keyObject = createSecretKey(new Uint8Array(16)); + const key = keyObject.toCryptoKey( + { name: 'AES-GCM' }, + true, + ['decrypt', 'encrypt', 'decrypt']); + assertSameSet(key.usages, ['encrypt', 'decrypt']); + assert.strictEqual(key.usages.length, 2); + })()); + + tests.push((async () => { + const keyObject = createSecretKey(new Uint8Array(32)); + const key = keyObject.toCryptoKey( + 'HKDF', + false, + ['deriveBits', 'deriveKey', 'deriveBits']); + assertSameSet(key.usages, ['deriveKey', 'deriveBits']); + assert.strictEqual(key.usages.length, 2); + })()); + + Promise.all(tests).then(common.mustCall()); +} + +{ + (async () => { + const key = await subtle.generateKey( + { name: 'AES-CTR', length: 128 }, + true, + ['wrapKey', 'encrypt', 'decrypt', 'encrypt', 'wrapKey', 'unwrapKey']); + // Regardless of the input order, de-duplicated usages are returned in + // a canonical order. + assert.deepStrictEqual( + key.usages, + ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']); + })().then(common.mustCall()); +} + +// Exported JWK `key_ops` must also be de-duplicated. +{ + const tests = []; + + const jwkVectors = [ + { algorithm: { name: 'HMAC', hash: 'SHA-256' }, + usages: ['verify', 'sign', 'verify', 'sign'], + expected: ['sign', 'verify'] }, + { algorithm: { name: 'AES-CBC', length: 128 }, + usages: ['decrypt', 'encrypt', 'decrypt', 'encrypt'], + expected: ['encrypt', 'decrypt'] }, + { algorithm: { name: 'AES-GCM', length: 128 }, + usages: ['decrypt', 'encrypt', 'decrypt'], + expected: ['encrypt', 'decrypt'] }, + ]; + + if (!process.features.openssl_is_boringssl) { + jwkVectors.push({ + algorithm: { name: 'AES-KW', length: 128 }, + usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], + expected: ['wrapKey', 'unwrapKey'], + }); + } else { + common.printSkipMessage('AES-KW is not supported in BoringSSL'); + } + + if (hasOpenSSL(3)) { + jwkVectors.push({ + algorithm: { name: 'AES-OCB', length: 128 }, + usages: ['decrypt', 'encrypt', 'decrypt', 'encrypt'], + expected: ['encrypt', 'decrypt'], + }); + jwkVectors.push({ + algorithm: { name: 'KMAC128', length: 128 }, + usages: ['verify', 'sign', 'verify', 'sign'], + expected: ['sign', 'verify'], + }); + } else { + common.printSkipMessage('AES-OCB and KMAC require OpenSSL >= 3'); + } + + for (const { algorithm, usages, expected } of jwkVectors) { + tests.push((async () => { + const key = await subtle.generateKey(algorithm, true, usages); + const jwk = await subtle.exportKey('jwk', key); + assertSameSet(jwk.key_ops, expected, + `jwk key_ops for ${algorithm.name}`); + assert.strictEqual(jwk.key_ops.length, expected.length, + `jwk key_ops length for ${algorithm.name}`); + })()); + } + + const jwkPairVectors = [ + { algorithm: { name: 'RSA-OAEP', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + usages: ['wrapKey', 'unwrapKey', 'decrypt', 'encrypt', + 'unwrapKey', 'wrapKey', 'decrypt', 'encrypt'], + publicExpected: ['encrypt', 'wrapKey'], + privateExpected: ['decrypt', 'unwrapKey'] }, + { algorithm: { name: 'RSA-PSS', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'ECDSA', namedCurve: 'P-256' }, + usages: ['verify', 'sign', 'verify', 'sign', 'verify'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'ECDH', namedCurve: 'P-256' }, + usages: ['deriveBits', 'deriveKey', 'deriveBits', 'deriveKey'], + publicExpected: undefined, + privateExpected: ['deriveKey', 'deriveBits'] }, + { algorithm: { name: 'Ed25519' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'] }, + { algorithm: { name: 'X25519' }, + usages: ['deriveBits', 'deriveKey', 'deriveBits', 'deriveKey'], + publicExpected: undefined, + privateExpected: ['deriveKey', 'deriveBits'] }, + ]; + + if (hasOpenSSL(3, 5)) { + jwkPairVectors.push({ + algorithm: { name: 'ML-DSA-65' }, + usages: ['verify', 'sign', 'verify', 'sign'], + publicExpected: ['verify'], + privateExpected: ['sign'], + }); + jwkPairVectors.push({ + algorithm: { name: 'ML-KEM-768' }, + usages: ['decapsulateBits', 'encapsulateBits', 'decapsulateKey', + 'encapsulateKey', 'decapsulateBits', 'encapsulateBits'], + publicExpected: ['encapsulateKey', 'encapsulateBits'], + privateExpected: ['decapsulateKey', 'decapsulateBits'], + }); + } else { + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + } + + for (const { algorithm, usages, publicExpected, privateExpected } of jwkPairVectors) { + tests.push((async () => { + const { publicKey, privateKey } = + await subtle.generateKey(algorithm, true, usages); + const publicJwk = await subtle.exportKey('jwk', publicKey); + const privateJwk = await subtle.exportKey('jwk', privateKey); + + if (publicExpected === undefined) { + // Empty public-key usages result in an empty `key_ops`. + assert.deepStrictEqual(publicJwk.key_ops, [], + `jwk key_ops for ${algorithm.name} publicKey`); + } else { + assertSameSet(publicJwk.key_ops, publicExpected, + `jwk key_ops for ${algorithm.name} publicKey`); + assert.strictEqual(publicJwk.key_ops.length, publicExpected.length); + } + + assertSameSet(privateJwk.key_ops, privateExpected, + `jwk key_ops for ${algorithm.name} privateKey`); + assert.strictEqual(privateJwk.key_ops.length, privateExpected.length); + })()); + } + + Promise.all(tests).then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 1baf2baee9952c..e57c34436578ab 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -853,8 +853,8 @@ if (hasOpenSSL(3, 5)) { const kTests = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; const tests = kTests.map((name) => test(name, - ['decapsulateBits', 'decapsulateKey'], - ['encapsulateBits', 'encapsulateKey'])); + ['decapsulateKey', 'decapsulateBits'], + ['encapsulateKey', 'encapsulateBits'])); Promise.all(tests).then(common.mustCall()); }