diff --git a/benchmark/crypto/create-keyobject.js b/benchmark/crypto/create-keyobject.js index 30f8213175df69..7cd6db2d567ad6 100644 --- a/benchmark/crypto/create-keyobject.js +++ b/benchmark/crypto/create-keyobject.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const bench = common.createBenchmark(main, { diff --git a/benchmark/crypto/kem.js b/benchmark/crypto/kem.js index e03ae65f1926ca..a544fc2124afe9 100644 --- a/benchmark/crypto/kem.js +++ b/benchmark/crypto/kem.js @@ -24,6 +24,9 @@ if (hasOpenSSL(3, 5)) { keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private'); keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private'); keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private_seed_only'); + keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private_seed_only'); } if (hasOpenSSL(3, 2)) { keyFixtures['p-256'] = readKeyPair('ec_p256_public', 'ec_p256_private'); @@ -54,9 +57,6 @@ const bench = common.createBenchmark(main, { // assess whether mutexes over the key material impact the operation if (p.keyFormat === 'keyObject.unique') return p.mode === 'async-parallel'; - // JWK is not supported for ml-kem for now - if (p.keyFormat === 'jwk') - return !p.keyType.startsWith('ml-'); // raw-public is only supported for encapsulate, not rsa if (p.keyFormat === 'raw-public') return p.keyType !== 'rsa' && p.op === 'encapsulate'; diff --git a/benchmark/crypto/oneshot-sign.js b/benchmark/crypto/oneshot-sign.js index d0abc7b5412e60..72e3726d9a5349 100644 --- a/benchmark/crypto/oneshot-sign.js +++ b/benchmark/crypto/oneshot-sign.js @@ -19,6 +19,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKey('ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/benchmark/crypto/oneshot-verify.js b/benchmark/crypto/oneshot-verify.js index c6a24f52126eb2..8b397b02dbf285 100644 --- a/benchmark/crypto/oneshot-verify.js +++ b/benchmark/crypto/oneshot-verify.js @@ -26,6 +26,8 @@ const keyFixtures = { if (hasOpenSSL(3, 5)) { keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private'); +} else if (process.features.openssl_is_boringssl) { + keyFixtures['ml-dsa-44'] = readKeyPair('ml_dsa_44_public', 'ml_dsa_44_private_seed_only'); } const data = crypto.randomBytes(256); diff --git a/benchmark/crypto/webcrypto-sign.js b/benchmark/crypto/webcrypto-sign.js new file mode 100644 index 00000000000000..d298ec69eef437 --- /dev/null +++ b/benchmark/crypto/webcrypto-sign.js @@ -0,0 +1,97 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { subtle } = globalThis.crypto; + +const kAlgorithms = { + 'ec': { name: 'ECDSA', namedCurve: 'P-256' }, + 'rsassa-pkcs1-v1_5': { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'rsa-pss': { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'ed25519': { name: 'Ed25519' }, +}; + +if (hasOpenSSL(3, 5)) { + kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' }; +} + +const kSignParams = { + 'ec': { name: 'ECDSA', hash: 'SHA-256' }, + 'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' }, + 'rsa-pss': { name: 'RSA-PSS', saltLength: 32 }, + 'ed25519': { name: 'Ed25519' }, + 'ml-dsa-44': { name: 'ML-DSA-44' }, +}; + +const data = globalThis.crypto.getRandomValues(new Uint8Array(256)); + +let keys; + +const bench = common.createBenchmark(main, { + keyType: Object.keys(kAlgorithms), + mode: ['serial', 'parallel'], + keyReuse: ['shared', 'unique'], + n: [1e3], +}, { + combinationFilter(p) { + // Unique only differs from shared when operations overlap (parallel); + // sequential calls have no contention so unique+serial adds no value. + if (p.keyReuse === 'unique') return p.mode === 'parallel'; + return true; + }, +}); + +async function measureSerial(n, signParams, sharedKey) { + bench.start(); + for (let i = 0; i < n; ++i) { + await subtle.sign(signParams, sharedKey || keys[i], data); + } + bench.end(n); +} + +async function measureParallel(n, signParams, sharedKey) { + const promises = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) { + promises[i] = subtle.sign(signParams, sharedKey || keys[i], data); + } + await Promise.all(promises); + bench.end(n); +} + +async function main({ n, mode, keyReuse, keyType }) { + const algorithm = kAlgorithms[keyType]; + const signParams = kSignParams[keyType]; + + if (!keys || keys.length !== n || keys[0].algorithm.name !== signParams.name) { + keys = new Array(n); + // Generate one key pair, then import its pkcs8 bytes n times to get + // distinct CryptoKey instances. + const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']); + const pkcs8 = await subtle.exportKey('pkcs8', kp.privateKey); + for (let i = 0; i < n; ++i) { + keys[i] = await subtle.importKey('pkcs8', pkcs8, algorithm, false, ['sign']); + } + } + + const sharedKey = keyReuse === 'shared' ? keys[0] : undefined; + + switch (mode) { + case 'serial': + await measureSerial(n, signParams, sharedKey); + break; + case 'parallel': + await measureParallel(n, signParams, sharedKey); + break; + } +} diff --git a/benchmark/crypto/webcrypto-verify.js b/benchmark/crypto/webcrypto-verify.js new file mode 100644 index 00000000000000..6b0d52cddc30d8 --- /dev/null +++ b/benchmark/crypto/webcrypto-verify.js @@ -0,0 +1,100 @@ +'use strict'; + +const common = require('../common.js'); +const { hasOpenSSL } = require('../../test/common/crypto.js'); +const { subtle } = globalThis.crypto; + +const kAlgorithms = { + 'ec': { name: 'ECDSA', namedCurve: 'P-256' }, + 'rsassa-pkcs1-v1_5': { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'rsa-pss': { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + 'ed25519': { name: 'Ed25519' }, +}; + +if (hasOpenSSL(3, 5)) { + kAlgorithms['ml-dsa-44'] = { name: 'ML-DSA-44' }; +} + +const kSignParams = { + 'ec': { name: 'ECDSA', hash: 'SHA-256' }, + 'rsassa-pkcs1-v1_5': { name: 'RSASSA-PKCS1-v1_5' }, + 'rsa-pss': { name: 'RSA-PSS', saltLength: 32 }, + 'ed25519': { name: 'Ed25519' }, + 'ml-dsa-44': { name: 'ML-DSA-44' }, +}; + +const data = globalThis.crypto.getRandomValues(new Uint8Array(256)); + +let publicKeys; +let signature; + +const bench = common.createBenchmark(main, { + keyType: Object.keys(kAlgorithms), + mode: ['serial', 'parallel'], + keyReuse: ['shared', 'unique'], + n: [1e3], +}, { + combinationFilter(p) { + // Unique only differs from shared when operations overlap (parallel); + // sequential calls have no contention so unique+serial adds no value. + if (p.keyReuse === 'unique') return p.mode === 'parallel'; + return true; + }, +}); + +async function measureSerial(n, verifyParams, sharedKey) { + bench.start(); + for (let i = 0; i < n; ++i) { + await subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data); + } + bench.end(n); +} + +async function measureParallel(n, verifyParams, sharedKey) { + const promises = new Array(n); + bench.start(); + for (let i = 0; i < n; ++i) { + promises[i] = subtle.verify(verifyParams, sharedKey || publicKeys[i], signature, data); + } + await Promise.all(promises); + bench.end(n); +} + +async function main({ n, mode, keyReuse, keyType }) { + const algorithm = kAlgorithms[keyType]; + const verifyParams = kSignParams[keyType]; + + if (!publicKeys || publicKeys.length !== n || + publicKeys[0].algorithm.name !== verifyParams.name) { + publicKeys = new Array(n); + // Generate one key pair, then import its spki bytes n times to get + // distinct CryptoKey instances. + const kp = await subtle.generateKey(algorithm, true, ['sign', 'verify']); + const spki = await subtle.exportKey('spki', kp.publicKey); + for (let i = 0; i < n; ++i) { + publicKeys[i] = await subtle.importKey('spki', spki, algorithm, false, ['verify']); + } + signature = await subtle.sign(verifyParams, kp.privateKey, data); + } + + const sharedKey = keyReuse === 'shared' ? publicKeys[0] : undefined; + + switch (mode) { + case 'serial': + await measureSerial(n, verifyParams, sharedKey); + break; + case 'parallel': + await measureParallel(n, verifyParams, sharedKey); + break; + } +} diff --git a/benchmark/misc/webcrypto-webidl.js b/benchmark/misc/webcrypto-webidl.js new file mode 100644 index 00000000000000..0f6275ed09517c --- /dev/null +++ b/benchmark/misc/webcrypto-webidl.js @@ -0,0 +1,109 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + op: [ + 'normalizeAlgorithm-string', + 'normalizeAlgorithm-dict', + 'webidl-dict', + 'webidl-algorithm-identifier-string', + 'webidl-algorithm-identifier-object', + 'webidl-dict-enforce-range', + 'webidl-dict-ensure-sha', + 'webidl-dict-null', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function main({ n, op }) { + const { normalizeAlgorithm } = require('internal/crypto/util'); + + switch (op) { + case 'normalizeAlgorithm-string': { + // String shortcut + null dictionary (cheapest path). + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm('SHA-256', 'digest'); + bench.end(n); + break; + } + case 'normalizeAlgorithm-dict': { + // Object input with a dictionary type and no BufferSource members. + const alg = { name: 'ECDSA', hash: 'SHA-256' }; + bench.start(); + for (let i = 0; i < n; i++) + normalizeAlgorithm(alg, 'sign'); + bench.end(n); + break; + } + case 'webidl-dict': { + // WebIDL dictionary converter in isolation. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'AES-GCM', iv: new Uint8Array(12) }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AeadParams(input, opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-string': { + // Exercises converters.AlgorithmIdentifier string path. + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier('SHA-256', opts); + bench.end(n); + break; + } + case 'webidl-algorithm-identifier-object': { + // Exercises converters.AlgorithmIdentifier object path. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.AlgorithmIdentifier(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-enforce-range': { + // Exercises [EnforceRange] integer dictionary members. + const webidl = require('internal/crypto/webidl'); + const input = { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaKeyGenParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-ensure-sha': { + // Exercises ensureSHA on a hash member. + const webidl = require('internal/crypto/webidl'); + const input = { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }; + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.RsaHashedImportParams(input, opts); + bench.end(n); + break; + } + case 'webidl-dict-null': { + // Exercises the null/undefined path in createDictionaryConverter(). + const webidl = require('internal/crypto/webidl'); + const opts = { prefix: 'test', context: 'test' }; + bench.start(); + for (let i = 0; i < n; i++) + webidl.converters.JsonWebKey(undefined, opts); + bench.end(n); + break; + } + } +} diff --git a/benchmark/misc/webidl-convert-to-int.js b/benchmark/misc/webidl-convert-to-int.js new file mode 100644 index 00000000000000..52245ee3b6a1a5 --- /dev/null +++ b/benchmark/misc/webidl-convert-to-int.js @@ -0,0 +1,82 @@ +'use strict'; + +const assert = require('assert'); +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + converter: [ + 'byte', + 'octet', + 'unsigned short', + 'unsigned long', + 'long long', + ], + input: [ + 'integer', + 'fractional', + 'wrap', + 'clamp', + 'enforce-range', + 'object', + ], + n: [1e6], +}, { flags: ['--expose-internals'] }); + +function getConverter(converter) { + switch (converter) { + case 'byte': + return { bitLength: 8, signedness: 'signed' }; + case 'octet': + return { bitLength: 8 }; + case 'unsigned short': + return { bitLength: 16 }; + case 'unsigned long': + return { bitLength: 32 }; + case 'long long': + return { bitLength: 64, signedness: 'signed' }; + default: + throw new Error(`Unsupported converter: ${converter}`); + } +} + +function getInput(input) { + switch (input) { + case 'integer': + return { value: 7 }; + case 'fractional': + return { value: 7.9 }; + case 'wrap': + return { value: 2 ** 63 + 2 ** 11 }; + case 'clamp': + return { value: 300.8, options: { clamp: true } }; + case 'enforce-range': + return { value: 7.9, options: { enforceRange: true } }; + case 'object': + return { + value: { + valueOf() { return 7; }, + }, + }; + default: + throw new Error(`Unsupported input: ${input}`); + } +} + +function main({ n, converter, input }) { + const { convertToInt } = require('internal/webidl'); + const { bitLength, signedness } = getConverter(converter); + const { value, options } = getInput(input); + + let noDead; + bench.start(); + if (options === undefined) { + for (let i = 0; i < n; i++) + noDead = convertToInt(value, bitLength, signedness); + } else { + for (let i = 0; i < n; i++) + noDead = convertToInt(value, bitLength, signedness, options); + } + bench.end(n); + + assert.strictEqual(typeof noDead, 'number'); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 07049405de6239..38378b730aca66 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -7,6 +7,11 @@ #include #include #include +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +#include +#include +#include +#endif #include #include #include @@ -15,7 +20,7 @@ #include #include #include -#if OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #include #endif #endif @@ -29,9 +34,13 @@ constexpr static PQCMapping pqc_mappings[] = { {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, + +#if OPENSSL_WITH_PQC_ML_KEM_512 + {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, @@ -44,6 +53,7 @@ constexpr static PQCMapping pqc_mappings[] = { {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, +#endif }; #endif @@ -67,6 +77,28 @@ using NetscapeSPKIPointer = DeleteFnPtr; static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON = XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL; + +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLCipher { + const EVP_CIPHER* (*get)(); + const char* name; +}; + +constexpr BoringSSLCipher kBoringSSLCiphers[] = { + {EVP_aes_128_cbc, "aes-128-cbc"}, {EVP_aes_128_ctr, "aes-128-ctr"}, + {EVP_aes_128_ecb, "aes-128-ecb"}, {EVP_aes_128_gcm, "aes-128-gcm"}, + {EVP_aes_128_ofb, "aes-128-ofb"}, {EVP_aes_192_cbc, "aes-192-cbc"}, + {EVP_aes_192_ctr, "aes-192-ctr"}, {EVP_aes_192_ecb, "aes-192-ecb"}, + {EVP_aes_192_gcm, "aes-192-gcm"}, {EVP_aes_192_ofb, "aes-192-ofb"}, + {EVP_aes_256_cbc, "aes-256-cbc"}, {EVP_aes_256_ctr, "aes-256-ctr"}, + {EVP_aes_256_ecb, "aes-256-ecb"}, {EVP_aes_256_gcm, "aes-256-gcm"}, + {EVP_aes_256_ofb, "aes-256-ofb"}, {EVP_des_cbc, "des-cbc"}, + {EVP_des_ecb, "des-ecb"}, {EVP_des_ede, "des-ede"}, + {EVP_des_ede3_cbc, "des-ede3-cbc"}, {EVP_des_ede_cbc, "des-ede-cbc"}, + {EVP_rc2_cbc, "rc2-cbc"}, {EVP_rc4, "rc4"}, +}; + +#endif } // namespace // ============================================================================ @@ -147,7 +179,12 @@ DataPointer DataPointer::SecureAlloc(size_t len) { #ifndef OPENSSL_IS_BORINGSSL auto ptr = OPENSSL_secure_zalloc(len); if (ptr == nullptr) return {}; - return DataPointer(ptr, len, true); + // OPENSSL_secure_zalloc transparently falls back to a regular allocation + // when the secure heap is not initialized or is exhausted. Reflect the + // actual provenance of the pointer so that reset() routes to the correct + // free function (OPENSSL_secure_clear_free vs. OPENSSL_clear_free) and + // callers of isSecure() get a truthful answer. + return DataPointer(ptr, len, CRYPTO_secure_allocated(ptr) == 1); #else // BoringSSL does not implement the OPENSSL_secure_zalloc API. auto ptr = OPENSSL_malloc(len); @@ -1923,8 +1960,7 @@ DataPointer pbkdf2(const Digest& md, return {}; } -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 DataPointer argon2(const Buffer& pass, const Buffer& salt, uint32_t lanes, @@ -2017,7 +2053,6 @@ DataPointer argon2(const Buffer& pass, return {}; } #endif -#endif // ============================================================================ @@ -2065,27 +2100,99 @@ EVPKeyPointer EVPKeyPointer::NewRawPrivate( } #if OPENSSL_WITH_PQC -EVPKeyPointer EVPKeyPointer::NewRawSeed( - int id, const Buffer& data) { - if (id == 0) return {}; +namespace { +constexpr size_t kPqcMlDsaSeedSize = 32; +constexpr size_t kPqcMlKemSeedSize = 64; + +size_t GetPqcSeedSize(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + return kPqcMlDsaSeedSize; +#if OPENSSL_WITH_PQC_ML_KEM_512 + case EVP_PKEY_ML_KEM_512: +#endif + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + return kPqcMlKemSeedSize; + default: + unreachable(); + } +} + +#if OPENSSL_WITH_BORINGSSL_PQC +const EVP_PKEY_ALG* GetPqcSeedAlg(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + return EVP_pkey_ml_dsa_44(); + case EVP_PKEY_ML_DSA_65: + return EVP_pkey_ml_dsa_65(); + case EVP_PKEY_ML_DSA_87: + return EVP_pkey_ml_dsa_87(); + case EVP_PKEY_ML_KEM_768: + return EVP_pkey_ml_kem_768(); + case EVP_PKEY_ML_KEM_1024: + return EVP_pkey_ml_kem_1024(); + default: + unreachable(); + } +} +#else +const char* GetPqcSeedParamName(int id) { + switch (id) { + case EVP_PKEY_ML_DSA_44: + case EVP_PKEY_ML_DSA_65: + case EVP_PKEY_ML_DSA_87: + return OSSL_PKEY_PARAM_ML_DSA_SEED; + case EVP_PKEY_ML_KEM_512: + case EVP_PKEY_ML_KEM_768: + case EVP_PKEY_ML_KEM_1024: + return OSSL_PKEY_PARAM_ML_KEM_SEED; + default: + unreachable(); + } +} +#endif +EVPKeyPointer NewPqcKeyFromSeed(int id, + const Buffer& data) { +#if OPENSSL_WITH_BORINGSSL_PQC + return EVPKeyPointer( + EVP_PKEY_from_private_seed(GetPqcSeedAlg(id), data.data, data.len)); +#else OSSL_PARAM params[] = { - OSSL_PARAM_construct_octet_string(OSSL_PKEY_PARAM_ML_DSA_SEED, + OSSL_PARAM_construct_octet_string(GetPqcSeedParamName(id), const_cast(data.data), data.len), OSSL_PARAM_END}; - EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(id, nullptr); - if (ctx == nullptr) return {}; + auto ctx = EVPKeyCtxPointer::NewFromID(id); + if (!ctx) return {}; EVP_PKEY* pkey = nullptr; - if (ctx == nullptr || EVP_PKEY_fromdata_init(ctx) <= 0 || - EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { - EVP_PKEY_CTX_free(ctx); + if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 || + EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params) <= 0) { return {}; } - return EVPKeyPointer(pkey); +#endif +} + +bool GetPqcSeed(EVP_PKEY* pkey, int id, const Buffer& out) { + size_t len = out.len; +#if OPENSSL_WITH_BORINGSSL_PQC + return EVP_PKEY_get_private_seed(pkey, out.data, &len) == 1; +#else + return EVP_PKEY_get_octet_string_param( + pkey, GetPqcSeedParamName(id), out.data, out.len, &len) == 1; +#endif +} +} // namespace + +EVPKeyPointer EVPKeyPointer::NewRawSeed( + int id, const Buffer& data) { + return NewPqcKeyFromSeed(id, data); } #endif @@ -2135,7 +2242,7 @@ EVP_PKEY* EVPKeyPointer::release() { int EVPKeyPointer::id(const EVP_PKEY* key) { if (key == nullptr) return 0; int type = EVP_PKEY_id(key); -#if OPENSSL_WITH_PQC +#if OPENSSL_WITH_OPENSSL_PQC // EVP_PKEY_id returns -1 when EVP_PKEY_* is only implemented in a provider // which is the case for all post-quantum NIST algorithms // one suggested way would be to use a chain of `EVP_PKEY_is_a` @@ -2213,34 +2320,11 @@ DataPointer EVPKeyPointer::rawPublicKey() const { DataPointer EVPKeyPointer::rawSeed() const { if (!pkey_) return {}; - // Determine seed length and parameter name based on key type - size_t seed_len; - const char* param_name; - - switch (id()) { - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - seed_len = 32; // ML-DSA uses 32-byte seeds - param_name = OSSL_PKEY_PARAM_ML_DSA_SEED; - break; - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - seed_len = 64; // ML-KEM uses 64-byte seeds - param_name = OSSL_PKEY_PARAM_ML_KEM_SEED; - break; - default: - unreachable(); - } + const size_t seed_len = GetPqcSeedSize(id()); if (auto data = DataPointer::Alloc(seed_len)) { const Buffer buf = data; - size_t len = data.size(); - - if (EVP_PKEY_get_octet_string_param( - get(), param_name, buf.data, len, &seed_len) != 1) - return {}; + if (!GetPqcSeed(get(), id(), buf)) return {}; return data; } return {}; @@ -2282,6 +2366,7 @@ EVPKeyPointer::operator const EC_KEY*() const { } namespace { + EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp, const char* name, auto&& parse) { @@ -2709,6 +2794,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_ML_DSA_44: case EVP_PKEY_ML_DSA_65: case EVP_PKEY_ML_DSA_87: +#if OPENSSL_WITH_PQC_SLH_DSA case EVP_PKEY_SLH_DSA_SHA2_128F: case EVP_PKEY_SLH_DSA_SHA2_128S: case EVP_PKEY_SLH_DSA_SHA2_192F: @@ -2721,6 +2807,7 @@ bool EVPKeyPointer::isOneShotVariant() const { case EVP_PKEY_SLH_DSA_SHAKE_192S: case EVP_PKEY_SLH_DSA_SHAKE_256F: case EVP_PKEY_SLH_DSA_SHAKE_256S: +#endif #endif return true; default: @@ -3103,9 +3190,13 @@ const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm); const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap); const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap); const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap); + +#ifndef OPENSSL_IS_BORINGSSL const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb); const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb); const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb); +#endif + const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); bool Cipher::isGcmMode() const { @@ -4200,6 +4291,12 @@ void Cipher::ForEach(Cipher::CipherNameCallback callback) { CipherCallbackContext context; context.cb = std::move(callback); +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& cipher : kBoringSSLCiphers) { + static_cast(cipher.get); + context.cb(cipher.name); + } +#else EVP_CIPHER_do_all_sorted( #if OPENSSL_VERSION_MAJOR >= 3 array_push_back, #endif &context); +#endif } // ============================================================================ @@ -4360,7 +4458,17 @@ std::optional EVPMDCtxPointer::signInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4405,7 +4513,17 @@ std::optional EVPMDCtxPointer::verifyInitWithContext( const EVPKeyPointer& key, const Digest& digest, const Buffer& context_string) { -#ifdef OSSL_SIGNATURE_PARAM_CONTEXT_STRING +#ifdef OPENSSL_IS_BORINGSSL + EVP_PKEY_CTX* ctx = nullptr; + if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) { + return std::nullopt; + } + if (EVP_PKEY_CTX_set1_signature_context_string( + ctx, context_string.data, context_string.len) <= 0) { + return std::nullopt; + } + return ctx; +#elif defined(OSSL_SIGNATURE_PARAM_CONTEXT_STRING) EVP_PKEY_CTX* ctx = nullptr; #ifdef OSSL_SIGNATURE_PARAM_INSTANCE @@ -4571,7 +4689,7 @@ HMACCtxPointer HMACCtxPointer::New() { return HMACCtxPointer(HMAC_CTX_new()); } -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {} EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept @@ -4659,7 +4777,7 @@ EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) { if (!mac) return EVPMacCtxPointer(); return EVPMacCtxPointer(EVP_MAC_CTX_new(mac)); } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC DataPointer hashDigest(const Buffer& buf, const EVP_MD* md) { @@ -4806,8 +4924,8 @@ const Digest Digest::FromName(const char* name) { // ============================================================================ // KEM Implementation -#if OPENSSL_VERSION_MAJOR >= 3 -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM +#if OPENSSL_WITH_KEM_OPERATION_PARAM bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { const char* operation = nullptr; @@ -4815,7 +4933,7 @@ bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) { case EVP_PKEY_RSA: operation = OSSL_KEM_PARAM_OPERATION_RSASVE; break; -#if OPENSSL_VERSION_PREREQ(3, 2) +#if OPENSSL_WITH_OPENSSL_DHKEM case EVP_PKEY_EC: case EVP_PKEY_X25519: case EVP_PKEY_X448: @@ -4852,7 +4970,7 @@ std::optional KEM::Encapsulate( return std::nullopt; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), public_key)) { return std::nullopt; } @@ -4893,7 +5011,7 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return {}; } -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM if (!SetOperationParameter(ctx.get(), private_key)) { return {}; } @@ -4923,6 +5041,6 @@ DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key, return shared_key; } -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KEM } // namespace ncrypto diff --git a/deps/ncrypto/ncrypto.gyp b/deps/ncrypto/ncrypto.gyp index cf9b7c6cdb6d2c..1747f3ea0149b9 100644 --- a/deps/ncrypto/ncrypto.gyp +++ b/deps/ncrypto/ncrypto.gyp @@ -1,5 +1,6 @@ { 'variables': { + 'ncrypto_bssl_libdecrepit_missing%': 1, 'ncrypto_sources': [ 'engine.cc', 'ncrypto.cc', @@ -11,8 +12,14 @@ 'target_name': 'ncrypto', 'type': 'static_library', 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], 'direct_dependent_settings': { 'include_dirs': ['.'], + 'defines': [ + 'NCRYPTO_BSSL_LIBDECREPIT_MISSING=<(ncrypto_bssl_libdecrepit_missing)', + ], }, 'sources': [ '<@(ncrypto_sources)' ], 'conditions': [ diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 4f86702da88267..b27e2e76c3dcfc 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -22,22 +22,103 @@ #ifndef OPENSSL_NO_ENGINE #include #endif // !OPENSSL_NO_ENGINE + +#ifndef OPENSSL_VERSION_PREREQ +#define OPENSSL_VERSION_PREREQ(maj, min) \ + (OPENSSL_VERSION_NUMBER >= (((maj) << 28) | ((min) << 20))) +#endif + +// BoringSSL declares the EVP_*_do_all* APIs, but their implementation may +// live in libdecrepit. This matches standalone ncrypto's build flag. +#ifndef NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_BSSL_LIBDECREPIT_MISSING 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) && NCRYPTO_BSSL_LIBDECREPIT_MISSING +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 1 +#else +#define NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK 0 +#endif + // The FIPS-related functions are only available // when the OpenSSL itself was compiled with FIPS support. -#if defined(OPENSSL_FIPS) && OPENSSL_VERSION_MAJOR < 3 +#if defined(OPENSSL_FIPS) && !OPENSSL_VERSION_PREREQ(3, 0) #include #endif // OPENSSL_FIPS -// Define OPENSSL_WITH_PQC for post-quantum cryptography support -#if OPENSSL_VERSION_NUMBER >= 0x30500000L -#define OPENSSL_WITH_PQC 1 +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_AES_OCB 1 +#else +#define OPENSSL_WITH_AES_OCB 0 +#endif + +#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_ARGON2 1 +#else +#define OPENSSL_WITH_ARGON2 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) || defined(OPENSSL_IS_BORINGSSL) +#define OPENSSL_WITH_KEM 1 +#else +#define OPENSSL_WITH_KEM 0 +#endif + +#if OPENSSL_VERSION_PREREQ(3, 0) +#define OPENSSL_WITH_KMAC 1 +#else +#define OPENSSL_WITH_KMAC 0 +#endif + +#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 1 +#else +#define OPENSSL_WITH_SIGNATURE_CONTEXT_STRING 0 +#endif + +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 2) +#define OPENSSL_WITH_OPENSSL_DHKEM 1 +#else +#define OPENSSL_WITH_OPENSSL_DHKEM 0 +#endif + +#if OPENSSL_WITH_KEM && !defined(OPENSSL_IS_BORINGSSL) && \ + !OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_KEM_OPERATION_PARAM 1 +#else +#define OPENSSL_WITH_KEM_OPERATION_PARAM 0 +#endif + +// Post-quantum cryptography support. Keep these explicit so code can +// distinguish provider API shape from the available algorithm set. +#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(3, 5) +#define OPENSSL_WITH_OPENSSL_PQC 1 +#else +#define OPENSSL_WITH_OPENSSL_PQC 0 +#endif + +#ifdef OPENSSL_IS_BORINGSSL +#define OPENSSL_WITH_BORINGSSL_PQC 1 +#else +#define OPENSSL_WITH_BORINGSSL_PQC 0 +#endif + +#define OPENSSL_WITH_PQC \ + (OPENSSL_WITH_OPENSSL_PQC || OPENSSL_WITH_BORINGSSL_PQC) +#define OPENSSL_WITH_PQC_ML_KEM_512 OPENSSL_WITH_OPENSSL_PQC +#define OPENSSL_WITH_PQC_SLH_DSA OPENSSL_WITH_OPENSSL_PQC + +#if OPENSSL_WITH_OPENSSL_PQC #define EVP_PKEY_ML_KEM_512 NID_ML_KEM_512 #define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 #define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #include +#elif OPENSSL_WITH_BORINGSSL_PQC +#define EVP_PKEY_ML_KEM_768 NID_ML_KEM_768 +#define EVP_PKEY_ML_KEM_1024 NID_ML_KEM_1024 #endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_VERSION_PREREQ(3, 0) #define OSSL3_CONST const #else #define OSSL3_CONST @@ -309,9 +390,12 @@ class Cipher final { #else static constexpr size_t MAX_AUTH_TAG_LENGTH = 16; #endif - static_assert(EVP_GCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && - EVP_CCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && - EVP_CHACHAPOLY_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH); + static_assert(EVP_GCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH +#ifndef OPENSSL_IS_BORINGSSL + && EVP_CCM_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH && + EVP_CHACHAPOLY_TLS_TAG_LEN <= MAX_AUTH_TAG_LENGTH +#endif + ); // NOLINT(whitespace/parens) Cipher() = default; Cipher(const EVP_CIPHER* cipher) : cipher_(cipher) {} @@ -1471,7 +1555,7 @@ class HMACCtxPointer final { DeleteFnPtr ctx_; }; -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC class EVPMacPointer final { public: EVPMacPointer() = default; @@ -1519,7 +1603,7 @@ class EVPMacCtxPointer final { private: DeleteFnPtr ctx_; }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KMAC #ifndef OPENSSL_NO_ENGINE class EnginePointer final { @@ -1632,8 +1716,7 @@ DataPointer pbkdf2(const Digest& md, uint32_t iterations, size_t length); -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 enum class Argon2Type { ARGON2D, ARGON2I, ARGON2ID }; DataPointer argon2(const Buffer& pass, @@ -1647,11 +1730,10 @@ DataPointer argon2(const Buffer& pass, const Buffer& ad, Argon2Type type); #endif -#endif // ============================================================================ // KEM (Key Encapsulation Mechanism) -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM class KEM final { public: @@ -1675,13 +1757,13 @@ class KEM final { const Buffer& ciphertext); private: -#if !OPENSSL_VERSION_PREREQ(3, 5) +#if OPENSSL_WITH_KEM_OPERATION_PARAM static bool SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key); #endif }; -#endif // OPENSSL_VERSION_MAJOR >= 3 +#endif // OPENSSL_WITH_KEM // ============================================================================ // Version metadata diff --git a/doc/api/crypto.md b/doc/api/crypto.md index a9b1bafa6749eb..779acf88a13e10 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -86,23 +86,23 @@ The following table lists the asymmetric key types recognized by the | `'ml-dsa-44'`[^openssl35] | ML-DSA-44 | 2.16.840.1.101.3.4.3.17 | ✔ | ✔ | ✔ | ✔ | | ✔ | | `'ml-dsa-65'`[^openssl35] | ML-DSA-65 | 2.16.840.1.101.3.4.3.18 | ✔ | ✔ | ✔ | ✔ | | ✔ | | `'ml-dsa-87'`[^openssl35] | ML-DSA-87 | 2.16.840.1.101.3.4.3.19 | ✔ | ✔ | ✔ | ✔ | | ✔ | -| `'ml-kem-512'`[^openssl35] | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | ✔ | ✔ | | ✔ | | ✔ | -| `'ml-kem-768'`[^openssl35] | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | ✔ | ✔ | | ✔ | | ✔ | -| `'ml-kem-1024'`[^openssl35] | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | ✔ | ✔ | | ✔ | | ✔ | +| `'ml-kem-512'`[^openssl35] | ML-KEM-512 | 2.16.840.1.101.3.4.4.1 | ✔ | ✔ | ✔ | ✔ | | ✔ | +| `'ml-kem-768'`[^openssl35] | ML-KEM-768 | 2.16.840.1.101.3.4.4.2 | ✔ | ✔ | ✔ | ✔ | | ✔ | +| `'ml-kem-1024'`[^openssl35] | ML-KEM-1024 | 2.16.840.1.101.3.4.4.3 | ✔ | ✔ | ✔ | ✔ | | ✔ | | `'rsa-pss'` | RSA PSS | 1.2.840.113549.1.1.10 | ✔ | ✔ | | | | | | `'rsa'` | RSA | 1.2.840.113549.1.1.1 | ✔ | ✔ | ✔ | | | | -| `'slh-dsa-sha2-128f'`[^openssl35] | SLH-DSA-SHA2-128f | 2.16.840.1.101.3.4.3.21 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-sha2-128s'`[^openssl35] | SLH-DSA-SHA2-128s | 2.16.840.1.101.3.4.3.20 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-sha2-192f'`[^openssl35] | SLH-DSA-SHA2-192f | 2.16.840.1.101.3.4.3.23 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-sha2-192s'`[^openssl35] | SLH-DSA-SHA2-192s | 2.16.840.1.101.3.4.3.22 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-sha2-256f'`[^openssl35] | SLH-DSA-SHA2-256f | 2.16.840.1.101.3.4.3.25 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-sha2-256s'`[^openssl35] | SLH-DSA-SHA2-256s | 2.16.840.1.101.3.4.3.24 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-128f'`[^openssl35] | SLH-DSA-SHAKE-128f | 2.16.840.1.101.3.4.3.27 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-128s'`[^openssl35] | SLH-DSA-SHAKE-128s | 2.16.840.1.101.3.4.3.26 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-192f'`[^openssl35] | SLH-DSA-SHAKE-192f | 2.16.840.1.101.3.4.3.29 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-192s'`[^openssl35] | SLH-DSA-SHAKE-192s | 2.16.840.1.101.3.4.3.28 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-256f'`[^openssl35] | SLH-DSA-SHAKE-256f | 2.16.840.1.101.3.4.3.31 | ✔ | ✔ | | ✔ | ✔ | | -| `'slh-dsa-shake-256s'`[^openssl35] | SLH-DSA-SHAKE-256s | 2.16.840.1.101.3.4.3.30 | ✔ | ✔ | | ✔ | ✔ | | +| `'slh-dsa-sha2-128f'`[^openssl35] | SLH-DSA-SHA2-128f | 2.16.840.1.101.3.4.3.21 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-sha2-128s'`[^openssl35] | SLH-DSA-SHA2-128s | 2.16.840.1.101.3.4.3.20 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-sha2-192f'`[^openssl35] | SLH-DSA-SHA2-192f | 2.16.840.1.101.3.4.3.23 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-sha2-192s'`[^openssl35] | SLH-DSA-SHA2-192s | 2.16.840.1.101.3.4.3.22 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-sha2-256f'`[^openssl35] | SLH-DSA-SHA2-256f | 2.16.840.1.101.3.4.3.25 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-sha2-256s'`[^openssl35] | SLH-DSA-SHA2-256s | 2.16.840.1.101.3.4.3.24 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-128f'`[^openssl35] | SLH-DSA-SHAKE-128f | 2.16.840.1.101.3.4.3.27 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-128s'`[^openssl35] | SLH-DSA-SHAKE-128s | 2.16.840.1.101.3.4.3.26 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-192f'`[^openssl35] | SLH-DSA-SHAKE-192f | 2.16.840.1.101.3.4.3.29 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-192s'`[^openssl35] | SLH-DSA-SHAKE-192s | 2.16.840.1.101.3.4.3.28 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-256f'`[^openssl35] | SLH-DSA-SHAKE-256f | 2.16.840.1.101.3.4.3.31 | ✔ | ✔ | ✔ | ✔ | ✔ | | +| `'slh-dsa-shake-256s'`[^openssl35] | SLH-DSA-SHAKE-256s | 2.16.840.1.101.3.4.3.30 | ✔ | ✔ | ✔ | ✔ | ✔ | | | `'x25519'` | X25519 | 1.3.101.110 | ✔ | ✔ | ✔ | ✔ | ✔ | | | `'x448'` | X448 | 1.3.101.111 | ✔ | ✔ | ✔ | ✔ | ✔ | | @@ -2393,6 +2393,10 @@ type, value, and parameters. This method is not -* `object` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} +* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject|CryptoKey} * `dsaEncoding` {string} * `padding` {integer} * `saltLength` {integer} @@ -2771,10 +2775,10 @@ changes: -Verifies the provided data using the given `object` and `signature`. +Verifies the provided data using the given `key` and `signature`. -If `object` is not a [`KeyObject`][], this function behaves as if -`object` had been passed to [`crypto.createPublicKey()`][]. If it is an +If `key` is not a [`KeyObject`][], this function behaves as if +`key` had been passed to [`crypto.createPublicKey()`][]. If it is an object, the following additional properties can be passed: * `dsaEncoding` {string} For DSA and ECDSA, this option specifies the @@ -3913,6 +3917,10 @@ input.on('readable', () => { * `options` {Object} - * `privateKey` {KeyObject} - * `publicKey` {KeyObject} + * `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} + * `publicKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} * `callback` {Function} * `err` {Error} * `secret` {Buffer} * Returns: {Buffer} if the `callback` function is not provided. Computes the Diffie-Hellman shared secret based on a `privateKey` and a `publicKey`. -Both keys must have the same `asymmetricKeyType` and must support either the DH or +Both keys must represent the same asymmetric key type and must support either the DH or ECDH operation. +If `options.privateKey` is not a [`KeyObject`][], this function behaves as if +`options.privateKey` had been passed to [`crypto.createPrivateKey()`][]. + +If `options.publicKey` is not a [`KeyObject`][], this function behaves as if +`options.publicKey` had been passed to [`crypto.createPublicKey()`][]. + If the `callback` function is provided this function uses libuv's threadpool. ### `crypto.encapsulate(key[, callback])` @@ -6924,7 +6945,7 @@ See the [list of SSL OP Flags][] for details. [`stream.transform` options]: stream.md#new-streamtransformoptions [`util.promisify()`]: util.md#utilpromisifyoriginal [`verify.update()`]: #verifyupdatedata-inputencoding -[`verify.verify()`]: #verifyverifyobject-signature-signatureencoding +[`verify.verify()`]: #verifyverifykey-signature-signatureencoding [`x509.fingerprint256`]: #x509fingerprint256 [`x509.verify(publicKey)`]: #x509verifypublickey [argon2]: https://www.rfc-editor.org/rfc/rfc9106.html diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 2b136308fd3843..585aa20524e620 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -4484,7 +4484,7 @@ will throw an error in a future version. [`Sign.prototype.sign()`]: crypto.md#signsignprivatekey-outputencoding [`SlowBuffer`]: buffer.md#class-slowbuffer [`String.prototype.toWellFormed`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toWellFormed -[`Verify.prototype.verify()`]: crypto.md#verifyverifyobject-signature-signatureencoding +[`Verify.prototype.verify()`]: crypto.md#verifyverifykey-signature-signatureencoding [`WriteStream.open()`]: fs.md#class-fswritestream [`assert.CallTracker`]: assert.md#class-assertcalltracker [`assert`]: assert.md diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index a987a8e664a3ac..521183f5755445 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,6 +2,10 @@ -* `algorithm` {string|Algorithm|CShakeParams} +* `algorithm` {string|Algorithm|CShakeParams|TurboShakeParams|KangarooTwelveParams} * `data` {ArrayBuffer|TypedArray|DataView|Buffer} * Returns: {Promise} Fulfills with an {ArrayBuffer} upon success. @@ -1018,6 +1034,8 @@ If `algorithm` is provided as a {string}, it must be one of: * `'cSHAKE128'`[^modern-algos] * `'cSHAKE256'`[^modern-algos] +* `'KT128'`[^modern-algos] +* `'KT256'`[^modern-algos] * `'SHA-1'` * `'SHA-256'` * `'SHA-384'` @@ -1025,6 +1043,8 @@ If `algorithm` is provided as a {string}, it must be one of: * `'SHA3-256'`[^modern-algos] * `'SHA3-384'`[^modern-algos] * `'SHA3-512'`[^modern-algos] +* `'TurboSHAKE128'`[^modern-algos] +* `'TurboSHAKE256'`[^modern-algos] If `algorithm` is provided as an {Object}, it must have a `name` property whose value is one of the above. @@ -1111,6 +1131,9 @@ The algorithms currently supported include: + +#### `kangarooTwelveParams.customization` + + + +* Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} + +The optional customization string for KangarooTwelve. + +#### `kangarooTwelveParams.name` + + + +* Type: {string} Must be `'KT128'`[^modern-algos] or `'KT256'`[^modern-algos] + +#### `kangarooTwelveParams.outputLength` + + + +* Type: {number} represents the requested output length in bits. + ### Class: `KmacImportParams` + +#### `turboShakeParams.domainSeparation` + + + +* Type: {number|undefined} + +The optional domain separation byte (0x01-0x7f). Defaults to `0x1f`. + +#### `turboShakeParams.name` + + + +* Type: {string} Must be `'TurboSHAKE128'`[^modern-algos] or `'TurboSHAKE256'`[^modern-algos] + +#### `turboShakeParams.outputLength` + + + +* Type: {number} represents the requested output length in bits. + [^secure-curves]: See [Secure Curves in the Web Cryptography API][] [^modern-algos]: See [Modern Algorithms in the Web Cryptography API][] diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index 3db7a1bbcc889e..3246683f73eeb4 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -61,7 +61,17 @@ export default [ rules: { 'prefer-object-spread': 'error', 'no-buffer-constructor': 'error', - 'no-restricted-syntax': noRestrictedSyntax, + 'no-restricted-syntax': [ + ...noRestrictedSyntax, + { + selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getCryptoKeySlots']", + message: "Use `const { getCryptoKeySlots } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.", + }, + { + selector: "VariableDeclarator[init.type='CallExpression'][init.callee.name='internalBinding'][init.arguments.0.value='crypto'] > ObjectPattern > Property[key.name='getKeyObjectSlots']", + message: "Use `const { getKeyObjectSlots } = require('internal/crypto/keys');` instead of destructuring it from `internalBinding('crypto')`.", + }, + ], 'no-restricted-globals': [ 'error', { @@ -392,6 +402,9 @@ export default [ 'node-core/lowercase-name-for-primitive': 'error', 'node-core/non-ascii-character': 'error', 'node-core/no-array-destructuring': 'error', + 'node-core/no-cryptokey-public-accessors': 'error', + 'node-core/no-keyobject-cryptokey-instanceof': 'error', + 'node-core/no-keyobject-public-accessors': 'error', 'node-core/prefer-primordials': [ 'error', { name: 'AggregateError' }, diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 5059b651f467ca..ab01484f1f7313 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -54,7 +54,6 @@ const { const { inspect } = require('internal/util/inspect'); const { converters, - convertToInt, createSequenceConverter, } = require('internal/webidl'); @@ -245,10 +244,14 @@ class Blob { if (!isBlob(this)) throw new ERR_INVALID_THIS('Blob'); - // Coerce values to int - const opts = { __proto__: null, signed: true }; - start = convertToInt('start', start, 64, opts); - end = convertToInt('end', end, 64, opts); + start = converters['long long']( + start, + { __proto__: null, context: 'start' }, + ); + end = converters['long long']( + end, + { __proto__: null, context: 'end' }, + ); if (start < 0) { start = MathMax(this[kLength] + start, 0); diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 7463faf1c98e48..2ed6c69f43e3d4 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -7,8 +7,7 @@ const { const { AESCipherJob, - KeyObjectHandle, - kCryptoJobAsync, + kCryptoJobWebCrypto, kKeyVariantAES_CTR_128, kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, @@ -24,33 +23,32 @@ const { kKeyVariantAES_GCM_256, kKeyVariantAES_KW_256, kKeyVariantAES_OCB_256, + SecretKeyGenJob, } = internalBinding('crypto'); const { + getUsagesMask, hasAnyNotIn, jobPromise, - validateKeyOps, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, - kAlgorithm, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getKeyObjectHandle, + getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); const { - generateKey: _generateKey, -} = require('internal/crypto/keygen'); - -const generateKey = promisify(_generateKey); + importJwkSecretKey, + importSecretKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function getAlgorithmName(name, length) { switch (name) { @@ -109,32 +107,32 @@ function getVariant(name, length) { function asyncAesCtrCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-CTR', key[kAlgorithm].length), + getVariant('AES-CTR', getCryptoKeyAlgorithm(key).length), algorithm.counter, algorithm.length)); } function asyncAesCbcCipher(mode, key, data, algorithm) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-CBC', key[kAlgorithm].length), + getVariant('AES-CBC', getCryptoKeyAlgorithm(key).length), algorithm.iv)); } function asyncAesKwCipher(mode, key, data) { return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-KW', key[kAlgorithm].length))); + getVariant('AES-KW', getCryptoKeyAlgorithm(key).length))); } function asyncAesGcmCipher(mode, key, data, algorithm) { @@ -142,11 +140,11 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-GCM', key[kAlgorithm].length), + getVariant('AES-GCM', getCryptoKeyAlgorithm(key).length), algorithm.iv, tagByteLength, algorithm.additionalData)); @@ -157,11 +155,11 @@ function asyncAesOcbCipher(mode, key, data, algorithm) { const tagByteLength = tagLength / 8; return jobPromise(() => new AESCipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, - getVariant('AES-OCB', key.algorithm.length), + getVariant('AES-OCB', getCryptoKeyAlgorithm(key).length), algorithm.iv, tagByteLength, algorithm.additionalData)); @@ -177,7 +175,7 @@ function aesCipher(mode, key, data, algorithm) { } } -async function aesGenerateKey(algorithm, extractable, keyUsages) { +function aesGenerateKey(algorithm, extractable, keyUsages) { const { name, length } = algorithm; const checkUsages = ['wrapKey', 'unwrapKey']; @@ -190,22 +188,18 @@ async function aesGenerateKey(algorithm, extractable, keyUsages) { 'Unsupported key usage for an AES key', 'SyntaxError'); } - - let key; - try { - key = await generateKey('aes', { length }); - } catch (err) { + if (usagesSet.size === 0) { throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); } - return new InternalCryptoKey( - key, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, - usagesSet, - extractable); + getUsagesMask(usagesSet), + extractable)); } function aesImportKey( @@ -226,12 +220,13 @@ function aesImportKey( 'SyntaxError'); } - let keyObject; + let handle; let length; switch (format) { case 'KeyObject': { - validateKeyLength(keyData.symmetricKeySize * 8); - keyObject = keyData; + length = getKeyObjectSymmetricKeySize(keyData) * 8; + validateKeyLength(length); + handle = getKeyObjectHandle(keyData); break; } case 'raw-secret': @@ -239,67 +234,32 @@ function aesImportKey( if (format === 'raw' && name === 'AES-OCB') { return undefined; } - validateKeyLength(keyData.byteLength * 8); - keyObject = createSecretKey(keyData); + length = keyData.byteLength * 8; + validateKeyLength(length); + handle = importSecretKey(keyData); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - - if (keyData.kty !== 'oct') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); - - if (usagesSet.size > 0 && - keyData.use !== undefined && - keyData.use !== 'enc') { - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } - - const handle = new KeyObjectHandle(); - try { - handle.initJwk(keyData); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - - ({ length } = handle.keyDetail({ })); + validateJwk(keyData, 'oct', extractable, usagesSet, 'enc'); + handle = importJwkSecretKey(keyData); + length = handle.getSymmetricKeySize() * 8; validateKeyLength(length); - if (keyData.alg !== undefined) { if (keyData.alg !== getAlgorithmName(algorithm.name, length)) throw lazyDOMException( 'JWK "alg" does not match the requested algorithm', 'DataError'); } - - keyObject = new SecretKeyObject(handle); break; } default: return undefined; } - if (length === undefined) { - ({ length } = keyObject[kHandle].keyDetail({ })); - validateKeyLength(length); - } - return new InternalCryptoKey( - keyObject, + handle, { name, length }, - usagesSet, + getUsagesMask(usagesSet), extractable); } diff --git a/lib/internal/crypto/argon2.js b/lib/internal/crypto/argon2.js index 1d2b8b7cac91e9..6d9f9e462d01fe 100644 --- a/lib/internal/crypto/argon2.js +++ b/lib/internal/crypto/argon2.js @@ -3,8 +3,6 @@ const { FunctionPrototypeCall, MathPow, - StringPrototypeToLowerCase, - TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; @@ -14,6 +12,7 @@ const { Argon2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, kTypeArgon2d, kTypeArgon2i, kTypeArgon2id, @@ -21,12 +20,15 @@ const { const { lazyDOMException, - promisify, } = require('internal/util'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); + const { getArrayBufferOrView, - kKeyObject, + jobPromise, } = require('internal/crypto/util'); const { @@ -140,20 +142,12 @@ function check(algorithm, parameters) { validateString(algorithm, 'algorithm'); validateOneOf(algorithm, 'algorithm', ['argon2d', 'argon2i', 'argon2id']); - let type; - switch (algorithm) { - case 'argon2d': - type = kTypeArgon2d; - break; - case 'argon2i': - type = kTypeArgon2i; - break; - case 'argon2id': - type = kTypeArgon2id; - break; - default: // unreachable - throw new ERR_CRYPTO_ARGON2_NOT_SUPPORTED(); - } + const type = { + '__proto__': null, + 'argon2d': kTypeArgon2d, + 'argon2i': kTypeArgon2i, + 'argon2id': kTypeArgon2id, + }[algorithm]; validateObject(parameters, 'parameters'); @@ -190,7 +184,6 @@ function check(algorithm, parameters) { return { message, nonce, secret, associatedData, tagLength, passes, parallelism, memory, type }; } -const argon2Promise = promisify(argon2); function validateArgon2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -208,31 +201,27 @@ function validateArgon2DeriveBitsLength(length) { } } -async function argon2DeriveBits(algorithm, baseKey, length) { +function argon2DeriveBits(algorithm, baseKey, length) { validateArgon2DeriveBitsLength(length); - let result; - try { - result = await argon2Promise( - StringPrototypeToLowerCase(algorithm.name), - { - message: baseKey[kKeyObject].export(), - nonce: algorithm.nonce, - parallelism: algorithm.parallelism, - tagLength: length / 8, - memory: algorithm.memory, - passes: algorithm.passes, - secret: algorithm.secretValue, - associatedData: algorithm.associatedData, - }, - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - - return TypedArrayPrototypeGetBuffer(result); + const type = { + '__proto__': null, + 'Argon2d': kTypeArgon2d, + 'Argon2i': kTypeArgon2i, + 'Argon2id': kTypeArgon2id, + }[algorithm.name]; + + return jobPromise(() => new Argon2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + algorithm.nonce, + algorithm.parallelism, + length / 8, + algorithm.memory, + algorithm.passes, + algorithm.secretValue === undefined ? new Uint8Array() : algorithm.secretValue, + algorithm.associatedData === undefined ? new Uint8Array() : algorithm.associatedData, + type)); } module.exports = { diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 5986ebd63038c9..cceed86a660216 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -3,55 +3,53 @@ const { SafeSet, StringPrototypeToLowerCase, + TypedArrayPrototypeGetBuffer, } = primordials; const { Buffer } = require('buffer'); const { - ECKeyExportJob, - KeyObjectHandle, SignJob, - kCryptoJobAsync, - kKeyTypePrivate, - kKeyTypePublic, + kCryptoJobWebCrypto, + kKeyFormatDER, + kKeyFormatRawPublic, kSignJobModeSign, kSignJobModeVerify, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ED25519, + EVP_PKEY_ED448, + EVP_PKEY_X25519, + EVP_PKEY_X448, } = internalBinding('crypto'); const { - codes: { - ERR_CRYPTO_INVALID_JWK, - }, -} = require('internal/errors'); - -const { + getUsagesMask, getUsagesUnion, hasAnyNotIn, jobPromise, - validateKeyOps, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectHandle, + getKeyObjectType, InternalCryptoKey, - PrivateKeyObject, - PublicKeyObject, - createPrivateKey, - createPublicKey, - kKeyType, } = require('internal/crypto/keys'); -const generateKeyPair = promisify(_generateKeyPair); +const { + importDerKey, + importJwkKey, + importRawKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { let checkSet; @@ -77,40 +75,7 @@ function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { } } -function createCFRGRawKey(name, keyData, isPublic) { - const handle = new KeyObjectHandle(); - - switch (name) { - case 'Ed25519': - case 'X25519': - if (keyData.byteLength !== 32) { - throw lazyDOMException( - `${name} raw keys must be exactly 32-bytes`, 'DataError'); - } - break; - case 'Ed448': - if (keyData.byteLength !== 57) { - throw lazyDOMException( - `${name} raw keys must be exactly 57-bytes`, 'DataError'); - } - break; - case 'X448': - if (keyData.byteLength !== 56) { - throw lazyDOMException( - `${name} raw keys must be exactly 56-bytes`, 'DataError'); - } - break; - } - - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initEDRaw(name, keyData, keyType)) { - throw lazyDOMException('Invalid keyData', 'DataError'); - } - - return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle); -} - -async function cfrgGenerateKey(algorithm, extractable, keyUsages) { +function cfrgGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -134,30 +99,13 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { } break; } - let genKeyType; - switch (name) { - case 'Ed25519': - genKeyType = 'ed25519'; - break; - case 'Ed448': - genKeyType = 'ed448'; - break; - case 'X25519': - genKeyType = 'x25519'; - break; - case 'X448': - genKeyType = 'x448'; - break; - } - - let keyPair; - try { - keyPair = await generateKeyPair(genKeyType); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const nid = { + '__proto__': null, + 'Ed25519': EVP_PKEY_ED25519, + 'Ed448': EVP_PKEY_ED448, + 'X25519': EVP_PKEY_X25519, + 'X448': EVP_PKEY_X448, + }[name]; let publicUsages; let privateUsages; @@ -178,28 +126,45 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - keyPair.publicKey, - keyAlgorithm, - publicUsages, - true); - - const privateKey = - new InternalCryptoKey( - keyPair.privateKey, - keyAlgorithm, - privateUsages, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function cfrgExportKey(key, format) { - return jobPromise(() => new ECKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle])); + try { + switch (format) { + case kWebCryptoKeyFormatRaw: { + const handle = getCryptoKeyHandle(key); + return TypedArrayPrototypeGetBuffer( + getCryptoKeyType(key) === 'private' ? handle.rawPrivateKey() : handle.rawPublicKey()); + } + case kWebCryptoKeyFormatSPKI: { + return TypedArrayPrototypeGetBuffer( + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function cfrgImportKey( @@ -210,151 +175,82 @@ function cfrgImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { - verifyAcceptableCfrgKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + verifyAcceptableCfrgKeyUse( + name, getKeyObjectType(keyData) === 'public', usagesSet); + handle = getKeyObjectHandle(keyData); break; } case 'spki': { verifyAcceptableCfrgKeyUse(name, true, usagesSet); - try { - keyObject = createPublicKey({ - key: keyData, - format: 'der', - type: 'spki', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableCfrgKeyUse(name, false, usagesSet); - try { - keyObject = createPrivateKey({ - key: keyData, - format: 'der', - type: 'pkcs8', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, false); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - if (keyData.kty !== 'OKP') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + const expectedUse = (name === 'X25519' || name === 'X448') ? 'enc' : 'sig'; + validateJwk(keyData, 'OKP', extractable, usagesSet, expectedUse); + if (keyData.crv !== name) throw lazyDOMException( 'JWK "crv" Parameter and algorithm name mismatch', 'DataError'); - const isPublic = keyData.d === undefined; - - if (usagesSet.size > 0 && keyData.use !== undefined) { - let checkUse; - switch (name) { - case 'Ed25519': - // Fall through - case 'Ed448': - checkUse = 'sig'; - break; - case 'X25519': - // Fall through - case 'X448': - checkUse = 'enc'; - break; - } - if (keyData.use !== checkUse) - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } if (keyData.alg !== undefined && (name === 'Ed25519' || name === 'Ed448')) { - if (keyData.alg !== name && keyData.alg !== 'EdDSA') { + if (keyData.alg !== name && keyData.alg !== 'EdDSA') throw lazyDOMException( - 'JWK "alg" does not match the requested algorithm', - 'DataError'); - } + 'JWK "alg" does not match the requested algorithm', 'DataError'); } - if (!isPublic && typeof keyData.x !== 'string') { - throw lazyDOMException('Invalid JWK', 'DataError'); - } - - verifyAcceptableCfrgKeyUse( - name, - isPublic, - usagesSet); - - try { - const publicKeyObject = createCFRGRawKey( - name, - Buffer.from(keyData.x, 'base64'), - true); - - if (isPublic) { - keyObject = publicKeyObject; - } else { - keyObject = createCFRGRawKey( - name, - Buffer.from(keyData.d, 'base64'), - false); + const isPublic = keyData.d === undefined; + verifyAcceptableCfrgKeyUse(name, isPublic, usagesSet); + handle = importJwkKey(isPublic, keyData); - if (!createPublicKey(keyObject).equals(publicKeyObject)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - } - } catch (err) { - throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); + if (!isPublic) { + const publicKey = Buffer.from(keyData.x, 'base64url'); + if (!Buffer.from(handle.rawPublicKey()).equals(publicKey)) + throw lazyDOMException('Invalid keyData', 'DataError'); } break; } case 'raw': { verifyAcceptableCfrgKeyUse(name, true, usagesSet); - keyObject = createCFRGRawKey(name, keyData, true); + handle = importRawKey(true, keyData, kKeyFormatRawPublic, name); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, - usagesSet, + getUsagesMask(usagesSet), extractable); } -async function eddsaSignVerify(key, data, algorithm, signature) { +function eddsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), + undefined, undefined, undefined, undefined, diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 9f9778c0d1e290..9d4606090af8ee 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -6,34 +6,31 @@ const { const { ChaCha20Poly1305CipherJob, - KeyObjectHandle, - kCryptoJobAsync, + SecretKeyGenJob, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { + getUsagesMask, hasAnyNotIn, jobPromise, - validateKeyOps, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, + getCryptoKeyHandle, + getKeyObjectHandle, } = require('internal/crypto/keys'); const { - randomBytes: _randomBytes, -} = require('internal/crypto/random'); - -const randomBytes = promisify(_randomBytes); + importJwkSecretKey, + importSecretKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function validateKeyLength(length) { if (length !== 256) @@ -42,15 +39,15 @@ function validateKeyLength(length) { function c20pCipher(mode, key, data, algorithm) { return jobPromise(() => new ChaCha20Poly1305CipherJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, algorithm.iv, algorithm.additionalData)); } -async function c20pGenerateKey(algorithm, extractable, keyUsages) { +function c20pGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; @@ -61,22 +58,18 @@ async function c20pGenerateKey(algorithm, extractable, keyUsages) { `Unsupported key usage for a ${algorithm.name} key`, 'SyntaxError'); } - - let keyData; - try { - keyData = await randomBytes(32); - } catch (err) { + if (usagesSet.size === 0) { throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); } - return new InternalCryptoKey( - createSecretKey(keyData), + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + 256, { name }, - usagesSet, - extractable); + getUsagesMask(usagesSet), + extractable)); } function c20pImportKey( @@ -95,46 +88,20 @@ function c20pImportKey( 'SyntaxError'); } - let keyObject; + let handle; switch (format) { case 'KeyObject': { - keyObject = keyData; + handle = getKeyObjectHandle(keyData); break; } case 'raw-secret': { - keyObject = createSecretKey(keyData); + handle = importSecretKey(keyData); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - - if (keyData.kty !== 'oct') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); - - if (usagesSet.size > 0 && - keyData.use !== undefined && - keyData.use !== 'enc') { - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } + validateJwk(keyData, 'oct', extractable, usagesSet, 'enc'); - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } - - const handle = new KeyObjectHandle(); - try { - handle.initJwk(keyData); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importJwkSecretKey(keyData); if (keyData.alg !== undefined && keyData.alg !== 'C20P') { throw lazyDOMException( @@ -142,19 +109,18 @@ function c20pImportKey( 'DataError'); } - keyObject = new SecretKeyObject(handle); break; } default: return undefined; } - validateKeyLength(keyObject.symmetricKeySize * 8); + validateKeyLength(handle.getSymmetricKeySize() * 8); return new InternalCryptoKey( - keyObject, + handle, { name }, - usagesSet, + getUsagesMask(usagesSet), extractable); } diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 2fa4bd6dfb9997..1aeec8275d7b2d 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -63,21 +63,22 @@ const { normalizeEncoding } = require('internal/util'); const { StringDecoder } = require('string_decoder'); function rsaFunctionFor(method, defaultPadding, keyType) { - return (options, buffer) => { - const { format, type, data, passphrase } = + const keyName = keyType === 'private' ? 'privateKey' : undefined; + return (key, buffer) => { + const { format, type, data, passphrase, namedCurve } = keyType === 'private' ? - preparePrivateKey(options) : - preparePublicOrPrivateKey(options); - const padding = options.padding || defaultPadding; - const { oaepHash, encoding } = options; - let { oaepLabel } = options; + preparePrivateKey(key, keyName) : + preparePublicOrPrivateKey(key, keyName); + const padding = key.padding || defaultPadding; + const { oaepHash, encoding } = key; + let { oaepLabel } = key; if (oaepHash !== undefined) validateString(oaepHash, 'key.oaepHash'); if (oaepLabel !== undefined) oaepLabel = getArrayBufferOrView(oaepLabel, 'key.oaepLabel', encoding); buffer = getArrayBufferOrView(buffer, 'buffer', encoding); - return method(data, format, type, passphrase, buffer, padding, oaepHash, - oaepLabel); + return method(data, format, type, passphrase, namedCurve, buffer, + padding, oaepHash, oaepLabel); }; } diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 9155fae02ee698..876b64077a2aae 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -17,10 +17,10 @@ const { DiffieHellman: _DiffieHellman, DiffieHellmanGroup: _DiffieHellmanGroup, ECDH: _ECDH, - ECDHBitsJob, ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -28,7 +28,6 @@ const { ERR_CRYPTO_ECDH_INVALID_FORMAT, ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY, ERR_CRYPTO_INCOMPATIBLE_KEY, - ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, }, @@ -47,21 +46,27 @@ const { } = require('internal/util/types'); const { + deprecate, lazyDOMException, } = require('internal/util'); const { - KeyObject, - kAlgorithm, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectAsymmetricKeyType, + getKeyObjectType, + isKeyObject, + preparePrivateKey, + preparePublicOrPrivateKey, } = require('internal/crypto/keys'); const { getArrayBufferOrView, jobPromise, + jobPromiseThen, toBuf, kHandle, - kKeyObject, } = require('internal/crypto/util'); const { @@ -231,7 +236,9 @@ function ECDH(curve) { ECDH.prototype.computeSecret = DiffieHellman.prototype.computeSecret; ECDH.prototype.setPrivateKey = DiffieHellman.prototype.setPrivateKey; -ECDH.prototype.setPublicKey = DiffieHellman.prototype.setPublicKey; +ECDH.prototype.setPublicKey = deprecate(DiffieHellman.prototype.setPublicKey, + 'ecdh.setPublicKey() is deprecated.', + 'DEP0031'); ECDH.prototype.getPrivateKey = DiffieHellman.prototype.getPrivateKey; ECDH.prototype.generateKeys = function generateKeys(encoding, format) { @@ -274,6 +281,17 @@ function getFormat(format) { const dhEnabledKeyTypes = new SafeSet(['dh', 'ec', 'x448', 'x25519']); +function getDirectKeyObjectInfo(key) { + if (!isKeyObject(key)) + return undefined; + + const type = getKeyObjectType(key); + if (type === 'secret') + return { type }; + + return { type, asymmetricKeyType: getKeyObjectAsymmetricKeyType(key) }; +} + function diffieHellman(options, callback) { validateObject(options, 'options'); @@ -281,31 +299,53 @@ function diffieHellman(options, callback) { validateFunction(callback, 'callback'); const { privateKey, publicKey } = options; - if (!(privateKey instanceof KeyObject)) + + if (privateKey === undefined) throw new ERR_INVALID_ARG_VALUE('options.privateKey', privateKey); - if (!(publicKey instanceof KeyObject)) + if (publicKey === undefined) throw new ERR_INVALID_ARG_VALUE('options.publicKey', publicKey); - if (privateKey.type !== 'private') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private'); - - if (publicKey.type !== 'public' && publicKey.type !== 'private') { - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type, - 'private or public'); + const privateKeyInfo = getDirectKeyObjectInfo(privateKey); + const publicKeyInfo = getDirectKeyObjectInfo(publicKey); + if (privateKeyInfo?.type === 'private' && + (publicKeyInfo?.type === 'public' || publicKeyInfo?.type === 'private')) { + const privateType = privateKeyInfo.asymmetricKeyType; + const publicType = publicKeyInfo.asymmetricKeyType; + if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', + `${privateType} and ${publicType}`); + } } - const privateType = privateKey.asymmetricKeyType; - const publicType = publicKey.asymmetricKeyType; - if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', - `${privateType} and ${publicType}`); - } + const { + data: pubData, + format: pubFormat, + type: pubType, + passphrase: pubPassphrase, + namedCurve: pubNamedCurve, + } = preparePublicOrPrivateKey(publicKey, 'options.publicKey'); + + const { + data: privData, + format: privFormat, + type: privType, + passphrase: privPassphrase, + namedCurve: privNamedCurve, + } = preparePrivateKey(privateKey, 'options.privateKey'); const job = new DHBitsJob( callback ? kCryptoJobAsync : kCryptoJobSync, - publicKey[kHandle], - privateKey[kHandle]); + pubData, + pubFormat, + pubType, + pubPassphrase, + pubNamedCurve, + privData, + privFormat, + privType, + privPassphrase, + privNamedCurve); if (!callback) { const { 0: err, 1: secret } = job.run(); @@ -325,57 +365,69 @@ function diffieHellman(options, callback) { let masks; // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. -async function ecdhDeriveBits(algorithm, baseKey, length) { +function ecdhDeriveBits(algorithm, baseKey, length) { const { 'public': key } = algorithm; - if (baseKey[kKeyType] !== 'private') { + if (getCryptoKeyType(baseKey) !== 'private') { throw lazyDOMException( 'baseKey must be a private key', 'InvalidAccessError'); } - if (key[kAlgorithm].name !== baseKey[kAlgorithm].name) { + const keyAlgorithm = getCryptoKeyAlgorithm(key); + const baseKeyAlgorithm = getCryptoKeyAlgorithm(baseKey); + if (keyAlgorithm.name !== baseKeyAlgorithm.name) { throw lazyDOMException( 'The public and private keys must be of the same type', 'InvalidAccessError'); } if ( - key[kAlgorithm].name === 'ECDH' && - key[kAlgorithm].namedCurve !== baseKey[kAlgorithm].namedCurve + keyAlgorithm.name === 'ECDH' && + keyAlgorithm.namedCurve !== baseKeyAlgorithm.namedCurve ) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } - const bits = await jobPromise(() => new ECDHBitsJob( - kCryptoJobAsync, - key[kKeyObject][kHandle], - baseKey[kKeyObject][kHandle])); + const bits = jobPromise(() => new DHBitsJob( + kCryptoJobWebCrypto, + getCryptoKeyHandle(key), + undefined, + undefined, + undefined, + undefined, + getCryptoKeyHandle(baseKey), + undefined, + undefined, + undefined, + undefined)); // If a length is not specified, return the full derived secret if (length === null) return bits; - // If the length is not a multiple of 8 the nearest ceiled - // multiple of 8 is sliced. - const sliceLength = MathCeil(length / 8); + return jobPromiseThen(bits, (bits) => { + // If the length is not a multiple of 8 the nearest ceiled + // multiple of 8 is sliced. + const sliceLength = MathCeil(length / 8); - const { byteLength } = bits; - // If the length is larger than the derived secret, throw. - if (byteLength < sliceLength) - throw lazyDOMException('derived bit length is too small', 'OperationError'); + const { byteLength } = bits; + // If the length is larger than the derived secret, throw. + if (byteLength < sliceLength) + throw lazyDOMException('derived bit length is too small', 'OperationError'); - const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); + const slice = ArrayBufferPrototypeSlice(bits, 0, sliceLength); - const mod = length % 8; - if (mod === 0) - return slice; + const mod = length % 8; + if (mod === 0) + return slice; - // eslint-disable-next-line no-sparse-arrays - masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; + // eslint-disable-next-line no-sparse-arrays + masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; - const masked = new Uint8Array(slice); - masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; - return TypedArrayPrototypeGetBuffer(masked); + const masked = new Uint8Array(slice); + masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; + return TypedArrayPrototypeGetBuffer(masked); + }); } module.exports = { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 0bb8d99ee33299..212ba75e0a9b11 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -2,49 +2,60 @@ const { SafeSet, + TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetByteLength, } = primordials; const { - ECKeyExportJob, + EcKeyPairGenJob, KeyObjectHandle, SignJob, - kCryptoJobAsync, - kKeyTypePrivate, + kCryptoJobWebCrypto, + kKeyFormatDER, + kKeyFormatRawPublic, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kSigEncP1363, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatSPKI, } = internalBinding('crypto'); const { + crypto: { + POINT_CONVERSION_UNCOMPRESSED, + }, +} = internalBinding('constants'); + +const { + getUsagesMask, getUsagesUnion, hasAnyNotIn, jobPromise, normalizeHashName, - validateKeyOps, - kHandle, - kKeyObject, kNamedCurveAliases, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); -const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - const { InternalCryptoKey, - PrivateKeyObject, - PublicKeyObject, - createPrivateKey, - createPublicKey, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectHandle, + getKeyObjectType, } = require('internal/crypto/keys'); -const generateKeyPair = promisify(_generateKeyPair); +const { + importDerKey, + importJwkKey, + importRawKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function verifyAcceptableEcKeyUse(name, isPublic, usages) { let checkSet; @@ -66,17 +77,7 @@ function verifyAcceptableEcKeyUse(name, isPublic, usages) { } } -function createECPublicKeyRaw(namedCurve, keyData) { - const handle = new KeyObjectHandle(); - - if (!handle.initECRaw(kNamedCurveAliases[namedCurve], keyData)) { - throw lazyDOMException('Invalid keyData', 'DataError'); - } - - return new PublicKeyObject(handle); -} - -async function ecGenerateKey(algorithm, extractable, keyUsages) { +function ecGenerateKey(algorithm, extractable, keyUsages) { const { name, namedCurve } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -97,15 +98,6 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { // Fall through } - let keyPair; - try { - keyPair = await generateKeyPair('ec', { namedCurve }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - let publicUsages; let privateUsages; switch (name) { @@ -121,28 +113,64 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) { const keyAlgorithm = { name, namedCurve }; - const publicKey = - new InternalCryptoKey( - keyPair.publicKey, - keyAlgorithm, - publicUsages, - true); - - const privateKey = - new InternalCryptoKey( - keyPair.privateKey, - keyAlgorithm, - privateUsages, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, publicKey, privateKey }; + return jobPromise(() => new EcKeyPairGenJob( + kCryptoJobWebCrypto, + namedCurve, + undefined, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function ecExportKey(key, format) { - return jobPromise(() => new ECKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle])); + try { + const handle = getCryptoKeyHandle(key); + switch (format) { + case kWebCryptoKeyFormatRaw: { + return TypedArrayPrototypeGetBuffer( + handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED)); + } + case kWebCryptoKeyFormatSPKI: { + let spki = handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI); + // WebCrypto requires uncompressed point format for SPKI exports. + // This is a very rare edge case dependent on the imported key + // using compressed point format. + // Expected SPKI DER byte lengths with uncompressed points: + // P-256: 91 = 26 bytes of SPKI ASN.1 + 65-byte uncompressed point. + // P-384: 120 = 23 bytes of SPKI ASN.1 + 97-byte uncompressed point. + // P-521: 158 = 25 bytes of SPKI ASN.1 + 133-byte uncompressed point. + // Difference in initial SPKI ASN.1 is caused by OIDs and length encoding. + const { namedCurve } = getCryptoKeyAlgorithm(key); + if (TypedArrayPrototypeGetByteLength(spki) !== { + '__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158, + }[namedCurve]) { + const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED); + const tmp = new KeyObjectHandle(); + tmp.init(kKeyTypePublic, raw, kKeyFormatRawPublic, + 'ec', null, namedCurve); + spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI); + } + return TypedArrayPrototypeGetBuffer(spki); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function ecImportKey( @@ -154,73 +182,34 @@ function ecImportKey( ) { const { name, namedCurve } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { - verifyAcceptableEcKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + verifyAcceptableEcKeyUse( + name, getKeyObjectType(keyData) === 'public', usagesSet); + handle = getKeyObjectHandle(keyData); break; } case 'spki': { verifyAcceptableEcKeyUse(name, true, usagesSet); - try { - keyObject = createPublicKey({ - key: keyData, - format: 'der', - type: 'spki', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableEcKeyUse(name, false, usagesSet); - try { - keyObject = createPrivateKey({ - key: keyData, - format: 'der', - type: 'pkcs8', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, false); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - if (keyData.kty !== 'EC') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + const expectedUse = name === 'ECDH' ? 'enc' : 'sig'; + validateJwk(keyData, 'EC', extractable, usagesSet, expectedUse); + if (keyData.crv !== namedCurve) throw lazyDOMException( 'JWK "crv" does not match the requested algorithm', 'DataError'); - verifyAcceptableEcKeyUse( - name, - keyData.d === undefined, - usagesSet); - - if (usagesSet.size > 0 && keyData.use !== undefined) { - const checkUse = name === 'ECDH' ? 'enc' : 'sig'; - if (keyData.use !== checkUse) - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } - if (algorithm.name === 'ECDSA' && keyData.alg !== undefined) { let algNamedCurve; switch (keyData.alg) { @@ -234,24 +223,14 @@ function ecImportKey( 'DataError'); } - const handle = new KeyObjectHandle(); - let type; - try { - type = handle.initJwk(keyData, namedCurve); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - if (type === undefined) - throw lazyDOMException('Invalid keyData', 'DataError'); - keyObject = type === kKeyTypePrivate ? - new PrivateKeyObject(handle) : - new PublicKeyObject(handle); + const isPublic = keyData.d === undefined; + verifyAcceptableEcKeyUse(name, isPublic, usagesSet); + handle = importJwkKey(isPublic, keyData); break; } case 'raw': { verifyAcceptableEcKeyUse(name, true, usagesSet); - keyObject = createECPublicKeyRaw(namedCurve, keyData); + handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve); break; } default: @@ -262,46 +241,42 @@ function ecImportKey( case 'ECDSA': // Fall through case 'ECDH': - if (keyObject.asymmetricKeyType !== 'ec') + if (handle.getAsymmetricKeyType() !== 'ec') throw lazyDOMException('Invalid key type', 'DataError'); break; } - if (!keyObject[kHandle].checkEcKeyData()) { + if (!handle.checkEcKeyData()) { throw lazyDOMException('Invalid keyData', 'DataError'); } - const { - namedCurve: checkNamedCurve, - } = keyObject[kHandle].keyDetail({}); - if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) + if (kNamedCurveAliases[namedCurve] !== handle.keyDetail({}).namedCurve) throw lazyDOMException('Named curve mismatch', 'DataError'); return new InternalCryptoKey( - keyObject, + handle, { name, namedCurve }, - usagesSet, + getUsagesMask(usagesSet), extractable); } -async function ecdsaSignVerify(key, data, { name, hash }, signature) { +function ecdsaSignVerify(key, data, { name, hash }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - const hashname = normalizeHashName(hash.name); - - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), + undefined, undefined, undefined, undefined, data, - hashname, + normalizeHashName(hash.name), undefined, // Salt length, not used with ECDSA undefined, // PSS Padding, not used with ECDSA kSigEncP1363, diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index eecbe5213f568c..37881a11c6dc2e 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -12,8 +12,10 @@ const { Hash: _Hash, HashJob, Hmac: _Hmac, - kCryptoJobAsync, + kCryptoJobWebCrypto, oneShotDigest, + TurboShakeJob, + KangarooTwelveJob, } = internalBinding('crypto'); const { @@ -200,7 +202,7 @@ Hmac.prototype._transform = Hash.prototype._transform; // Implementation for WebCrypto subtle.digest() -async function asyncDigest(algorithm, data) { +function asyncDigest(algorithm, data) { validateMaxBufferLength(data, 'data'); switch (algorithm.name) { @@ -221,11 +223,29 @@ async function asyncDigest(algorithm, data) { case 'cSHAKE128': // Fall through case 'cSHAKE256': - return await jobPromise(() => new HashJob( - kCryptoJobAsync, + return jobPromise(() => new HashJob( + kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, algorithm.outputLength)); + case 'TurboSHAKE128': + // Fall through + case 'TurboSHAKE256': + return jobPromise(() => new TurboShakeJob( + kCryptoJobWebCrypto, + algorithm.name, + algorithm.domainSeparation ?? 0x1f, + algorithm.outputLength / 8, + data)); + case 'KT128': + // Fall through + case 'KT256': + return jobPromise(() => new KangarooTwelveJob( + kCryptoJobWebCrypto, + algorithm.name, + algorithm.customization, + algorithm.outputLength / 8, + data)); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 1a5e9ccd06813e..73b16da6923024 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -3,12 +3,14 @@ const { ArrayBuffer, FunctionPrototypeCall, + PromiseResolve, } = primordials; const { HKDFJob, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -20,20 +22,20 @@ const { const { kMaxLength } = require('buffer'); const { + jobPromise, normalizeHashName, toBuf, validateByteSource, - kKeyObject, } = require('internal/crypto/util'); const { - createSecretKey, + getCryptoKeyHandle, isKeyObject, + prepareSecretKey, } = require('internal/crypto/keys'); const { lazyDOMException, - promisify, } = require('internal/util'); const { @@ -75,10 +77,10 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { function prepareKey(key) { if (isKeyObject(key)) - return key; + return prepareSecretKey(key); if (isAnyArrayBuffer(key)) - return createSecretKey(key); + return key; key = toBuf(key); @@ -96,7 +98,7 @@ function prepareKey(key) { key); } - return createSecretKey(key); + return key; } function hkdf(hash, key, salt, info, length, callback) { @@ -137,7 +139,6 @@ function hkdfSync(hash, key, salt, info, length) { return bits; } -const hkdfPromise = promisify(hkdf); function validateHkdfDeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -148,22 +149,20 @@ function validateHkdfDeriveBitsLength(length) { } } -async function hkdfDeriveBits(algorithm, baseKey, length) { +function hkdfDeriveBits(algorithm, baseKey, length) { validateHkdfDeriveBitsLength(length); const { hash, salt, info } = algorithm; if (length === 0) - return new ArrayBuffer(0); + return PromiseResolve(new ArrayBuffer(0)); - try { - return await hkdfPromise( - normalizeHashName(hash.name), baseKey[kKeyObject], salt, info, length / 8, - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return jobPromise(() => new HKDFJob( + kCryptoJobWebCrypto, + normalizeHashName(hash.name), + getCryptoKeyHandle(baseKey), + salt, + info, + length / 8)); } module.exports = { diff --git a/lib/internal/crypto/kem.js b/lib/internal/crypto/kem.js index 43c7bde52ea99f..d38f8b1c59a050 100644 --- a/lib/internal/crypto/kem.js +++ b/lib/internal/crypto/kem.js @@ -42,6 +42,7 @@ function encapsulate(key, callback) { format: keyFormat, type: keyType, passphrase: keyPassphrase, + namedCurve: keyNamedCurve, } = preparePublicOrPrivateKey(key); const job = new KEMEncapsulateJob( @@ -49,7 +50,8 @@ function encapsulate(key, callback) { keyData, keyFormat, keyType, - keyPassphrase); + keyPassphrase, + keyNamedCurve); if (!callback) { const { 0: err, 1: result } = job.run(); @@ -79,6 +81,7 @@ function decapsulate(key, ciphertext, callback) { format: keyFormat, type: keyType, passphrase: keyPassphrase, + namedCurve: keyNamedCurve, } = preparePrivateKey(key); ciphertext = getArrayBufferOrView(ciphertext, 'ciphertext'); @@ -89,6 +92,7 @@ function decapsulate(key, ciphertext, callback) { keyFormat, keyType, keyPassphrase, + keyNamedCurve, ciphertext); if (!callback) { diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 62f1616e6449b2..515a31141913df 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -148,7 +148,7 @@ function parseKeyEncoding(keyType, options = kEmptyObject) { format: publicFormat, type: publicType, } = parsePublicKeyEncoding(publicKeyEncoding, keyType, - 'publicKeyEncoding')); + 'options.publicKeyEncoding')); } else { throw new ERR_INVALID_ARG_VALUE('options.publicKeyEncoding', publicKeyEncoding); @@ -164,7 +164,7 @@ function parseKeyEncoding(keyType, options = kEmptyObject) { cipher, passphrase, } = parsePrivateKeyEncoding(privateKeyEncoding, keyType, - 'privateKeyEncoding')); + 'options.privateKeyEncoding')); } else { throw new ERR_INVALID_ARG_VALUE('options.privateKeyEncoding', privateKeyEncoding); diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 2fbf4f306d8433..2722ecc0520e2c 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -1,13 +1,11 @@ 'use strict'; const { - ArrayFrom, ArrayPrototypeSlice, ObjectDefineProperties, - ObjectDefineProperty, + ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, SafeSet, - Symbol, SymbolToStringTag, Uint8Array, } = primordials; @@ -15,6 +13,11 @@ const { const { KeyObjectHandle, createNativeKeyObjectClass, + // eslint-disable-next-line no-restricted-syntax -- intended here + getKeyObjectSlots: nativeGetKeyObjectSlots, + createCryptoKeyClass, + // eslint-disable-next-line no-restricted-syntax -- intended here + getCryptoKeySlots: nativeGetCryptoKeySlots, kKeyTypeSecret, kKeyTypePublic, kKeyTypePrivate, @@ -46,7 +49,6 @@ const { const { codes: { ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, - ERR_CRYPTO_INVALID_JWK, ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, @@ -56,13 +58,13 @@ const { } = require('internal/errors'); const { - kHandle, - kKeyObject, getArrayBufferOrView, bigIntArrayToUnsignedBigInt, normalizeAlgorithm, hasAnyNotIn, - getSortedUsages, + getUsagesMask, + getUsagesFromMask, + hasUsage, } = require('internal/crypto/util'); const { @@ -70,29 +72,28 @@ const { isArrayBufferView, } = require('internal/util/types'); -const { - markTransferMode, - kClone, - kDeserialize, -} = require('internal/worker/js_transferable'); - const { customInspectSymbol: kInspect, + getDeprecationWarningEmitter, kEnumerableProperty, + kEmptyObject, lazyDOMException, } = require('internal/util'); const { inspect } = require('internal/util/inspect'); -const { Buffer } = require('buffer'); - -const kAlgorithm = Symbol('kAlgorithm'); -const kExtractable = Symbol('kExtractable'); -const kKeyType = Symbol('kKeyType'); -const kKeyUsages = Symbol('kKeyUsages'); -const kCachedAlgorithm = Symbol('kCachedAlgorithm'); -const kCachedKeyUsages = Symbol('kCachedKeyUsages'); +const emitDEP0203 = getDeprecationWarningEmitter( + 'DEP0203', + 'Passing a CryptoKey to node:crypto functions is deprecated.', +); +const maybeEmitDEP0204 = getDeprecationWarningEmitter( + 'DEP0204', + 'Passing a non-extractable CryptoKey to KeyObject.from() is deprecated.', + undefined, + false, + (key) => !getCryptoKeyExtractable(key), +); // Key input contexts. const kConsumePublic = 0; @@ -105,6 +106,32 @@ for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) encodingNames[m[0]] = m[1]; +// KeyObject state lives on the native NativeKeyObject base class. JS reads +// the native type enum and a KeyObjectHandle in one call and caches that +// slot tuple in a private field so no forgeable own Symbols are exposed on +// public KeyObject instances. +let getKeyObjectSlots; // Populated by the createNativeKeyObjectClass callback. + +const kKeyObjectSlotType = 0; +const kKeyObjectSlotHandle = 1; +// The native slot tuple stops at kKeyObjectSlotHandle. The remaining entries +// are JS-side lazy cache slots derived from the KeyObjectHandle on first use. +const kKeyObjectSlotSymmetricKeySize = 2; +const kKeyObjectSlotAsymmetricKeyType = 3; +const kKeyObjectSlotAsymmetricKeyDetails = 4; + +function normalizeKeyDetails(details = kEmptyObject) { + if (details.publicExponent !== undefined) { + return { + __proto__: null, + ...details, + publicExponent: + bigIntArrayToUnsignedBigInt(new Uint8Array(details.publicExponent)), + }; + } + return details; +} + // Creating the KeyObject class is a little complicated due to inheritance // and the fact that KeyObjects should be transferable between threads, // which requires the KeyObject base class to be implemented in C++. @@ -118,6 +145,8 @@ const { } = createNativeKeyObjectClass((NativeKeyObject) => { // Publicly visible KeyObject class. class KeyObject extends NativeKeyObject { + #slots; + constructor(type, handle) { if (type !== 'secret' && type !== 'public' && type !== 'private') throw new ERR_INVALID_ARG_VALUE('type', type); @@ -125,26 +154,24 @@ const { throw new ERR_INVALID_ARG_TYPE('handle', 'object', handle); super(handle); - - this[kKeyType] = type; - - ObjectDefineProperty(this, kHandle, { - __proto__: null, - value: handle, - enumerable: false, - configurable: false, - writable: false, - }); } get type() { - return this[kKeyType]; + return getKeyObjectType(this); } static from(key) { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); - return key[kKeyObject]; + maybeEmitDEP0204(key); + const handle = getCryptoKeyHandle(key); + switch (getCryptoKeyType(key)) { + /* eslint-disable no-use-before-define */ + case 'secret': return new SecretKeyObject(handle); + case 'public': return new PublicKeyObject(handle); + case 'private': return new PrivateKeyObject(handle); + /* eslint-enable no-use-before-define */ + } } equals(otherKeyObject) { @@ -153,8 +180,25 @@ const { 'otherKeyObject', 'KeyObject', otherKeyObject); } - return otherKeyObject.type === this.type && - this[kHandle].equals(otherKeyObject[kHandle]); + const slots = getKeyObjectSlots(this); + const otherSlots = getKeyObjectSlots(otherKeyObject); + return slots[kKeyObjectSlotType] === otherSlots[kKeyObjectSlotType] && + slots[kKeyObjectSlotHandle].equals( + otherSlots[kKeyObjectSlotHandle]); + } + + static { + getKeyObjectSlots = (key) => { + if (!key || typeof key !== 'object') + throw new ERR_INVALID_THIS('KeyObject'); + if (#slots in key) { + const cached = key.#slots; + if (cached !== undefined) return cached; + } + const slots = nativeGetKeyObjectSlots(key); + key.#slots = slots; + return slots; + }; } } @@ -174,19 +218,20 @@ const { } get symmetricKeySize() { - return this[kHandle].getSymmetricKeySize(); + return getKeyObjectSymmetricKeySize(this); } export(options) { + const handle = getKeyObjectHandle(this); if (options !== undefined) { validateObject(options, 'options'); validateOneOf( options.format, 'options.format', [undefined, 'buffer', 'jwk']); if (options.format === 'jwk') { - return this[kHandle].exportJwk({}, false); + return handle.exportJwk({}, false); } } - return this[kHandle].export(); + return handle.export(); } toCryptoKey(algorithm, extractable, keyUsages) { @@ -241,9 +286,9 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result[kKeyUsages].length === 0) { + if (getCryptoKeyUsagesMask(result) === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, 'SyntaxError'); } @@ -251,37 +296,13 @@ const { } } - const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); - const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails'); - - function normalizeKeyDetails(details = {}) { - if (details.publicExponent !== undefined) { - return { - ...details, - publicExponent: - bigIntArrayToUnsignedBigInt(new Uint8Array(details.publicExponent)), - }; - } - return details; - } - class AsymmetricKeyObject extends KeyObject { get asymmetricKeyType() { - return this[kAsymmetricKeyType] ||= this[kHandle].getAsymmetricKeyType(); + return getKeyObjectAsymmetricKeyType(this); } get asymmetricKeyDetails() { - switch (this.asymmetricKeyType) { - case 'rsa': - case 'rsa-pss': - case 'dsa': - case 'ec': - return this[kAsymmetricKeyDetails] ||= normalizeKeyDetails( - this[kHandle].keyDetail({}), - ); - default: - return {}; - } + return { ...getKeyObjectAsymmetricKeyDetails(this) }; } toCryptoKey(algorithm, extractable, keyUsages) { @@ -336,9 +357,10 @@ const { throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - if (result.type === 'private' && result[kKeyUsages].length === 0) { + const resultType = getCryptoKeyType(result); + if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${resultType} key.`, 'SyntaxError'); } @@ -354,23 +376,27 @@ const { export(options) { switch (options?.format) { case 'jwk': - return this[kHandle].exportJwk({}, false); + return getKeyObjectHandle(this).exportJwk({}, false); case 'raw-public': { - if (this.asymmetricKeyType === 'ec') { + const handle = getKeyObjectHandle(this); + const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); + if (asymmetricKeyType === 'ec') { const { type = 'uncompressed' } = options; validateOneOf(type, 'options.type', ['compressed', 'uncompressed']); const form = type === 'compressed' ? POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED; - return this[kHandle].exportECPublicRaw(form); + return handle.exportECPublicRaw(form); } - return this[kHandle].rawPublicKey(); + return handle.rawPublicKey(); } default: { + const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); + const handle = getKeyObjectHandle(this); const { format, type, - } = parsePublicKeyEncoding(options, this.asymmetricKeyType); - return this[kHandle].export(format, type); + } = parsePublicKeyEncoding(options, asymmetricKeyType); + return handle.export(format, type); } } } @@ -389,23 +415,27 @@ const { } switch (options?.format) { case 'jwk': - return this[kHandle].exportJwk({}, false); + return getKeyObjectHandle(this).exportJwk({}, false); case 'raw-private': { - if (this.asymmetricKeyType === 'ec') { - return this[kHandle].exportECPrivateRaw(); + const handle = getKeyObjectHandle(this); + const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); + if (asymmetricKeyType === 'ec') { + return handle.exportECPrivateRaw(); } - return this[kHandle].rawPrivateKey(); + return handle.rawPrivateKey(); } case 'raw-seed': - return this[kHandle].rawSeed(); + return getKeyObjectHandle(this).rawSeed(); default: { + const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); + const handle = getKeyObjectHandle(this); const { format, type, cipher, passphrase, - } = parsePrivateKeyEncoding(options, this.asymmetricKeyType); - return this[kHandle].export(format, type, cipher, passphrase); + } = parsePrivateKeyEncoding(options, asymmetricKeyType); + return handle.export(format, type, cipher, passphrase); } } } @@ -456,9 +486,9 @@ function parseKeyType(typeStr, required, keyType, isPublic, optionName) { throw new ERR_INVALID_ARG_VALUE(optionName, typeStr); } -function option(name, objName) { - return objName === undefined ? - `options.${name}` : `options.${objName}.${name}`; +function option(name, prefix) { + return prefix === undefined ? + `options.${name}` : `${prefix}.${name}`; } function parseKeyFormatAndType(enc, keyType, isPublic, objName) { @@ -578,7 +608,7 @@ function parsePrivateKeyEncoding(enc, keyType, objName) { return parseKeyEncoding(enc, keyType, false, objName); } -function getKeyObjectHandle(key, ctx) { +function validateAsymmetricKeyType(type, ctx, key) { if (ctx === kCreatePrivate) { throw new ERR_INVALID_ARG_TYPE( 'key', @@ -587,16 +617,14 @@ function getKeyObjectHandle(key, ctx) { ); } - if (key.type !== 'private') { + if (type !== 'private') { if (ctx === kConsumePrivate || ctx === kCreatePublic) - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private'); - if (key.type !== 'public') { - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private'); + if (type !== 'public') { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private or public'); } } - - return key[kHandle]; } function getKeyTypes(allowKeyObject, bufferOnly = false) { @@ -617,259 +645,70 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { return types; } -function mlDsaPubLen(alg) { - switch (alg) { - case 'ML-DSA-44': return 1312; - case 'ML-DSA-65': return 1952; - case 'ML-DSA-87': return 2592; - } -} - -function getKeyObjectHandleFromJwk(key, ctx) { - validateObject(key, 'key'); - validateOneOf( - key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']); - const isPublic = ctx === kConsumePublic || ctx === kCreatePublic; - - if (key.kty === 'AKP') { - validateOneOf( - key.alg, 'key.alg', ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']); - validateString(key.pub, 'key.pub'); - - let keyData; - if (isPublic) { - keyData = Buffer.from(key.pub, 'base64url'); - if (keyData.byteLength !== mlDsaPubLen(key.alg)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - } else { - validateString(key.priv, 'key.priv'); - keyData = Buffer.from(key.priv, 'base64url'); - if (keyData.byteLength !== 32) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - } - - const handle = new KeyObjectHandle(); - - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initPqcRaw(key.alg, keyData, keyType)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - - return handle; - } - - if (key.kty === 'OKP') { - validateString(key.crv, 'key.crv'); - validateOneOf( - key.crv, 'key.crv', ['Ed25519', 'Ed448', 'X25519', 'X448']); - validateString(key.x, 'key.x'); - - if (!isPublic) - validateString(key.d, 'key.d'); - - let keyData; - if (isPublic) - keyData = Buffer.from(key.x, 'base64'); - else - keyData = Buffer.from(key.d, 'base64'); - - switch (key.crv) { - case 'Ed25519': - case 'X25519': - if (keyData.byteLength !== 32) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - break; - case 'Ed448': - if (keyData.byteLength !== 57) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - break; - case 'X448': - if (keyData.byteLength !== 56) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - break; - } - - const handle = new KeyObjectHandle(); - - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initEDRaw(key.crv, keyData, keyType)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - - return handle; - } - - if (key.kty === 'EC') { - validateString(key.crv, 'key.crv'); - validateOneOf( - key.crv, 'key.crv', ['P-256', 'secp256k1', 'P-384', 'P-521']); - validateString(key.x, 'key.x'); - validateString(key.y, 'key.y'); - - const jwk = { - kty: key.kty, - crv: key.crv, - x: key.x, - y: key.y, - }; - if (!isPublic) { - validateString(key.d, 'key.d'); - jwk.d = key.d; - } - - const handle = new KeyObjectHandle(); - const type = handle.initJwk(jwk, jwk.crv); - if (type === undefined) - throw new ERR_CRYPTO_INVALID_JWK(); - - return handle; - } - - // RSA - validateString(key.n, 'key.n'); - validateString(key.e, 'key.e'); - - const jwk = { - kty: key.kty, - n: key.n, - e: key.e, - }; - - if (!isPublic) { - validateString(key.d, 'key.d'); - validateString(key.p, 'key.p'); - validateString(key.q, 'key.q'); - validateString(key.dp, 'key.dp'); - validateString(key.dq, 'key.dq'); - validateString(key.qi, 'key.qi'); - jwk.d = key.d; - jwk.p = key.p; - jwk.q = key.q; - jwk.dp = key.dp; - jwk.dq = key.dq; - jwk.qi = key.qi; - } - - const handle = new KeyObjectHandle(); - const type = handle.initJwk(jwk); - if (type === undefined) - throw new ERR_CRYPTO_INVALID_JWK(); - - return handle; -} - - -function getKeyObjectHandleFromRaw(options, data, format) { - if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) { - throw new ERR_INVALID_ARG_TYPE( - 'key.key', - ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], - data); - } - - const keyData = getArrayBufferOrView(data, 'key.key'); - - validateString(options.asymmetricKeyType, 'key.asymmetricKeyType'); - const asymmetricKeyType = options.asymmetricKeyType; - - const handle = new KeyObjectHandle(); - - switch (asymmetricKeyType) { - case 'ec': { - validateString(options.namedCurve, 'key.namedCurve'); - if (format === 'raw-public') { - if (!handle.initECRaw(options.namedCurve, keyData)) { - throw new ERR_INVALID_ARG_VALUE('key.key', keyData); - } - } else if (!handle.initECPrivateRaw(options.namedCurve, keyData)) { - throw new ERR_INVALID_ARG_VALUE('key.key', keyData); - } - return handle; - } - case 'ed25519': - case 'ed448': - case 'x25519': - case 'x448': { - const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initEDRaw(asymmetricKeyType, keyData, keyType)) { - throw new ERR_INVALID_ARG_VALUE('key.key', keyData); - } - return handle; - } - case 'rsa': - case 'rsa-pss': - case 'dsa': - case 'dh': - throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( - format, `is not supported for ${asymmetricKeyType} keys`); - case 'ml-dsa-44': - case 'ml-dsa-65': - case 'ml-dsa-87': - case 'ml-kem-512': - case 'ml-kem-768': - case 'ml-kem-1024': - case 'slh-dsa-sha2-128f': - case 'slh-dsa-sha2-128s': - case 'slh-dsa-sha2-192f': - case 'slh-dsa-sha2-192s': - case 'slh-dsa-sha2-256f': - case 'slh-dsa-sha2-256s': - case 'slh-dsa-shake-128f': - case 'slh-dsa-shake-128s': - case 'slh-dsa-shake-192f': - case 'slh-dsa-shake-192s': - case 'slh-dsa-shake-256f': - case 'slh-dsa-shake-256s': { - const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initPqcRaw(asymmetricKeyType, keyData, keyType)) { - throw new ERR_INVALID_ARG_VALUE('key.key', keyData); - } - return handle; - } - default: - throw new ERR_INVALID_ARG_VALUE('asymmetricKeyType', asymmetricKeyType); - } -} - -function prepareAsymmetricKey(key, ctx) { +function prepareAsymmetricKey(key, ctx, name = 'key') { if (isKeyObject(key)) { // Best case: A key object, as simple as that. - return { data: getKeyObjectHandle(key, ctx) }; - } else if (isCryptoKey(key)) { - return { data: getKeyObjectHandle(key[kKeyObject], ctx) }; - } else if (isStringOrBuffer(key)) { + const type = getKeyObjectType(key); + validateAsymmetricKeyType(type, ctx, key); + return { data: getKeyObjectHandle(key) }; + } + if (isCryptoKey(key)) { + emitDEP0203(); + validateAsymmetricKeyType(getCryptoKeyType(key), ctx, key); + return { data: getCryptoKeyHandle(key) }; + } + if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. - return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; - } else if (typeof key === 'object') { + return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, name) }; + } + if (typeof key === 'object') { const { key: data, encoding, format } = key; // The 'key' property can be a KeyObject as well to allow specifying // additional options such as padding along with the key. - if (isKeyObject(data)) - return { data: getKeyObjectHandle(data, ctx) }; - else if (isCryptoKey(data)) - return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; - else if (format === 'jwk') { - validateObject(data, 'key.key'); - return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' }; + if (isKeyObject(data)) { + const type = getKeyObjectType(data); + validateAsymmetricKeyType(type, ctx, data); + return { data: getKeyObjectHandle(data) }; + } + if (isCryptoKey(data)) { + emitDEP0203(); + validateAsymmetricKeyType(getCryptoKeyType(data), ctx, data); + return { data: getCryptoKeyHandle(data) }; + } + if (format === 'jwk') { + validateObject(data, `${name}.key`); + return { data, format: kKeyFormatJWK }; } else if (format === 'raw-public' || format === 'raw-private' || format === 'raw-seed') { + if ((ctx === kConsumePrivate || ctx === kCreatePrivate) && + format === 'raw-public') { + throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format); + } + if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) { + throw new ERR_INVALID_ARG_TYPE( + `${name}.key`, + ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], + data); + } + validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`); + if (key.asymmetricKeyType === 'ec') { + validateString(key.namedCurve, `${name}.namedCurve`); + } + const rawFormat = parseKeyFormat(format, undefined, `${name}.format`); return { - data: getKeyObjectHandleFromRaw(key, data, format), - format, + data: getArrayBufferOrView(data, `${name}.key`), + format: rawFormat, + type: key.asymmetricKeyType, + namedCurve: key.namedCurve ?? null, }; } // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( - 'key.key', + `${name}.key`, getKeyTypes(ctx !== kCreatePrivate), data); } @@ -877,34 +716,39 @@ function prepareAsymmetricKey(key, ctx) { const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; return { - data: getArrayBufferOrView(data, 'key', encoding), - ...parseKeyEncoding(key, undefined, isPublic), + data: getArrayBufferOrView(data, `${name}.key`, encoding), + ...parseKeyEncoding(key, undefined, isPublic, name), }; } + throw new ERR_INVALID_ARG_TYPE( - 'key', + name, getKeyTypes(ctx !== kCreatePrivate), key); } -function preparePrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePrivate); +function preparePrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePrivate, name); } -function preparePublicOrPrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePublic); +function preparePublicOrPrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePublic, name); } function prepareSecretKey(key, encoding, bufferOnly = false) { if (!bufferOnly) { if (isKeyObject(key)) { - if (key.type !== 'secret') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'secret'); - return key[kHandle]; - } else if (isCryptoKey(key)) { - if (key[kKeyType] !== 'secret') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key[kKeyType], 'secret'); - return key[kKeyObject][kHandle]; + const type = getKeyObjectType(key); + if (type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'secret'); + return getKeyObjectHandle(key); + } + if (isCryptoKey(key)) { + emitDEP0203(); + const type = getCryptoKeyType(key); + if (type !== 'secret') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'secret'); + return getCryptoKeyHandle(key); } } if (typeof key !== 'string' && @@ -926,206 +770,418 @@ function createSecretKey(key, encoding) { } function createPublicKey(key) { - const { format, type, data, passphrase } = + const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePublic); - let handle; - if (format === 'jwk' || format === 'raw-public') { - handle = data; - } else { - handle = new KeyObjectHandle(); - handle.init(kKeyTypePublic, data, format, type, passphrase); - } + const handle = new KeyObjectHandle(); + handle.init(kKeyTypePublic, data, format ?? null, + type ?? null, passphrase ?? null, namedCurve ?? null); return new PublicKeyObject(handle); } function createPrivateKey(key) { - const { format, type, data, passphrase } = + const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePrivate); - let handle; - if (format === 'jwk' || format === 'raw-private' || format === 'raw-seed') { - handle = data; - } else { - handle = new KeyObjectHandle(); - handle.init(kKeyTypePrivate, data, format, type, passphrase); - } + const handle = new KeyObjectHandle(); + handle.init(kKeyTypePrivate, data, format ?? null, + type ?? null, passphrase ?? null, namedCurve ?? null); return new PrivateKeyObject(handle); } -function isKeyObject(obj) { - return obj != null && obj[kKeyType] !== undefined && obj[kKeyObject] === undefined; +function keyObjectTypeToString(type) { + switch (type) { + case kKeyTypeSecret: return 'secret'; + case kKeyTypePublic: return 'public'; + case kKeyTypePrivate: return 'private'; + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + } } -// Our implementation of CryptoKey is a simple wrapper around a KeyObject -// that adapts it to the standard interface. -// TODO(@jasnell): Embedder environments like electron may have issues -// here similar to other things like URL. A chromium provided CryptoKey -// will not be recognized as a Node.js CryptoKey, and vice versa. It -// would be fantastic if we could find a way of making those interop. -class CryptoKey { - constructor() { - throw new ERR_ILLEGAL_CONSTRUCTOR(); - } +// The helpers below return a KeyObject's native-backed slot values, +// populating the per-instance cache on first access via a single native +// call. The public getters delegate to these helpers, and internal +// consumers use them directly to avoid user-replaceable public accessors. +// Derived metadata such as key size and asymmetric key details is expanded +// lazily from the cached KeyObjectHandle. The public asymmetric key details +// getter returns a clone so the cached details object stays internal. - [kInspect](depth, options) { - if (depth < 0) - return this; +/** + * Returns the KeyObject's native type slot as a string. + * @param {KeyObject} key + * @returns {'secret' | 'public' | 'private'} + */ +function getKeyObjectType(key) { + return keyObjectTypeToString(getKeyObjectSlots(key)[kKeyObjectSlotType]); +} - const opts = { - ...options, - depth: options.depth == null ? null : options.depth - 1, - }; +/** + * Returns the KeyObjectHandle wrapping the KeyObject's underlying key + * material. + * @param {KeyObject} key + * @returns {KeyObjectHandle} + */ +function getKeyObjectHandle(key) { + return getKeyObjectSlots(key)[kKeyObjectSlotHandle]; +} - return `CryptoKey ${inspect({ - type: this[kKeyType], - extractable: this[kExtractable], - algorithm: this[kAlgorithm], - usages: this[kKeyUsages], - }, opts)}`; +/** + * Returns the KeyObject's symmetric key size, bypassing the public + * `symmetricKeySize` getter. The value is derived lazily from the cached + * KeyObjectHandle. + * @param {SecretKeyObject} key + * @returns {number} + */ +function getKeyObjectSymmetricKeySize(key) { + const slots = getKeyObjectSlots(key); + if (slots[kKeyObjectSlotType] !== kKeyTypeSecret) + throw new ERR_INVALID_THIS('SecretKeyObject'); + + let cached = slots[kKeyObjectSlotSymmetricKeySize]; + if (cached === undefined) { + cached = slots[kKeyObjectSlotHandle].getSymmetricKeySize(); + slots[kKeyObjectSlotSymmetricKeySize] = cached; } + return cached; +} - get [kKeyType]() { - return this[kKeyObject].type; +/** + * Returns the KeyObject's asymmetric key type, bypassing the public + * `asymmetricKeyType` getter. The value is derived lazily from the cached + * KeyObjectHandle. + * @param {PublicKeyObject|PrivateKeyObject} key + * @returns {string} + */ +function getKeyObjectAsymmetricKeyType(key) { + const slots = getKeyObjectSlots(key); + if (slots[kKeyObjectSlotType] === kKeyTypeSecret) + throw new ERR_INVALID_THIS('AsymmetricKeyObject'); + + let cached = slots[kKeyObjectSlotAsymmetricKeyType]; + if (cached === undefined) { + cached = slots[kKeyObjectSlotHandle].getAsymmetricKeyType(); + slots[kKeyObjectSlotAsymmetricKeyType] = cached; } + return cached; +} - get type() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - return this[kKeyType]; +/** + * Returns the KeyObject's cached asymmetric key details, bypassing the + * public `asymmetricKeyDetails` getter (which returns a cloned copy). + * The value is derived lazily from the cached KeyObjectHandle. + * @param {PublicKeyObject|PrivateKeyObject} key + * @returns {object} + */ +function getKeyObjectAsymmetricKeyDetails(key) { + const slots = getKeyObjectSlots(key); + if (slots[kKeyObjectSlotType] === kKeyTypeSecret) + throw new ERR_INVALID_THIS('AsymmetricKeyObject'); + + let cached = slots[kKeyObjectSlotAsymmetricKeyDetails]; + if (cached === undefined) { + let asymmetricKeyType = slots[kKeyObjectSlotAsymmetricKeyType]; + if (asymmetricKeyType === undefined) { + asymmetricKeyType = slots[kKeyObjectSlotHandle].getAsymmetricKeyType(); + slots[kKeyObjectSlotAsymmetricKeyType] = asymmetricKeyType; + } + switch (asymmetricKeyType) { + case 'rsa': + case 'rsa-pss': + case 'dsa': + case 'ec': + cached = normalizeKeyDetails( + slots[kKeyObjectSlotHandle].keyDetail({ __proto__: null }), + ); + break; + default: + cached = kEmptyObject; + break; + } + slots[kKeyObjectSlotAsymmetricKeyDetails] = cached; } + return cached; +} - get extractable() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - return this[kExtractable]; +function isKeyObject(obj) { + if (obj == null || typeof obj !== 'object') + return false; + + try { + getKeyObjectSlots(obj); + return true; + } catch { + return false; } +} - get algorithm() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - if (!this[kCachedAlgorithm]) { - this[kCachedAlgorithm] ??= { ...this[kAlgorithm] }; - this[kCachedAlgorithm].hash &&= { ...this[kCachedAlgorithm].hash }; - this[kCachedAlgorithm].publicExponent &&= new Uint8Array(this[kCachedAlgorithm].publicExponent); - } - return this[kCachedAlgorithm]; +// CryptoKey is a plain JS class whose prototype's [[Prototype]] is +// Object.prototype, as Web Crypto requires. Instance storage (type enum, +// extractable, algorithm, usages mask, and the KeyObject handle) lives +// on a C++ class, NativeCryptoKey, created by createCryptoKeyClass. +// InternalCryptoKey is the only constructor we expose to internal +// code; it extends NativeCryptoKey to get that storage and then has +// its prototype spliced so the chain visible to user code is: +// instance -> InternalCryptoKey.prototype +// -> CryptoKey.prototype +// -> Object.prototype +// +// All five internal slots are read from C++ in a single call via +// `getCryptoKeySlots`. The resulting array is cached in a private +// class field on `InternalCryptoKey` so that it is invisible to +// reflection (`Object.getOwnPropertySymbols` etc.) and leaves each +// CryptoKey's hidden class pristine. The `getCryptoKey{Type, +// Extractable,Algorithm,Usages,Handle}` helpers index into that +// array and convert native enums/masks back to Web Crypto strings. +// The internal algorithm object is stored as a null-prototype clone +// so it cannot observe polluted Object.prototype properties. +// The public `algorithm` getter caches a cloned dictionary and the +// public `usages` getter caches a synthesized array (as Web Crypto +// requires repeat reads to return the same object so a consumer's +// mutation is visible next time). +let getSlots; // Populated by the createCryptoKeyClass callback below. + +const kSlotType = 0; +const kSlotExtractable = 1; +const kSlotAlgorithm = 2; +const kSlotUsagesMask = 3; +const kSlotHandle = 4; +const kSlotClonedAlgorithm = 5; +const kSlotClonedUsages = 6; +const kSlotUsages = 7; + +function cloneAlgorithm(raw) { + const cloned = { ...raw }; + if (ObjectPrototypeHasOwnProperty(cloned, 'hash') && + cloned.hash !== undefined) { + cloned.hash = { ...cloned.hash }; } + if (ObjectPrototypeHasOwnProperty(cloned, 'publicExponent') && + cloned.publicExponent !== undefined) { + cloned.publicExponent = new Uint8Array(cloned.publicExponent); + } + return cloned; +} - get usages() { - if (!(this instanceof CryptoKey)) - throw new ERR_INVALID_THIS('CryptoKey'); - this[kCachedKeyUsages] ??= ArrayFrom(this[kKeyUsages]); - return this[kCachedKeyUsages]; +function cloneInternalAlgorithm(raw) { + const cloned = { __proto__: null, ...raw }; + if (ObjectPrototypeHasOwnProperty(cloned, 'hash') && + cloned.hash !== undefined) { + cloned.hash = { __proto__: null, ...cloned.hash }; + } + if (ObjectPrototypeHasOwnProperty(cloned, 'publicExponent') && + cloned.publicExponent !== undefined) { + cloned.publicExponent = new Uint8Array(cloned.publicExponent); } + return cloned; } -ObjectDefineProperties(CryptoKey.prototype, { - type: kEnumerableProperty, - extractable: kEnumerableProperty, - algorithm: kEnumerableProperty, - usages: kEnumerableProperty, - [SymbolToStringTag]: { - __proto__: null, - configurable: true, - value: 'CryptoKey', - }, -}); +const { + 0: CryptoKey, + 1: InternalCryptoKey, +} = createCryptoKeyClass((NativeCryptoKey) => { + class CryptoKey { + constructor() { + throw new ERR_ILLEGAL_CONSTRUCTOR(); + } -/** - * @param {InternalCryptoKey} key - * @param {KeyObject} keyObject - * @param {object} algorithm - * @param {boolean} extractable - * @param {Set} keyUsages - */ -function defineCryptoKeyProperties( - key, - keyObject, - algorithm, - extractable, - keyUsages, -) { - // Using symbol properties here currently instead of private - // properties because (for now) the performance penalty of - // private fields is still too high. - ObjectDefineProperties(key, { - [kKeyObject]: { - __proto__: null, - value: keyObject, - enumerable: false, - configurable: false, - writable: false, - }, - [kAlgorithm]: { - __proto__: null, - value: algorithm, - enumerable: false, - configurable: false, - writable: false, - }, - [kExtractable]: { - __proto__: null, - value: extractable, - enumerable: false, - configurable: false, - writable: false, - }, - [kKeyUsages]: { + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `CryptoKey ${inspect({ + type: getCryptoKeyType(this), + extractable: getCryptoKeyExtractable(this), + algorithm: cloneAlgorithm(getCryptoKeyAlgorithm(this)), + usages: ArrayPrototypeSlice(getCryptoKeyUsages(this), 0), + }, opts)}`; + } + + get type() { + return getCryptoKeyType(this); + } + + get extractable() { + return getCryptoKeyExtractable(this); + } + + get algorithm() { + const slots = getSlots(this); + let cached = slots[kSlotClonedAlgorithm]; + if (cached === undefined) { + cached = cloneAlgorithm(slots[kSlotAlgorithm]); + slots[kSlotClonedAlgorithm] = cached; + } + return cached; + } + + get usages() { + const slots = getSlots(this); + let cached = slots[kSlotClonedUsages]; + if (cached === undefined) { + cached = ArrayPrototypeSlice(getCryptoKeyUsagesFromSlots(slots), 0); + slots[kSlotClonedUsages] = cached; + } + return cached; + } + } + + class InternalCryptoKey extends NativeCryptoKey { + #slots; + + constructor(handle, algorithm, usagesMask, extractable) { + if (algorithm !== undefined) + algorithm = cloneInternalAlgorithm(algorithm); + super(handle, algorithm, usagesMask, extractable); + } + + static { + getSlots = (key) => { + if (!key || typeof key !== 'object') + throw new ERR_INVALID_THIS('CryptoKey'); + if (#slots in key) { + const cached = key.#slots; + if (cached !== undefined) return cached; + } + const slots = nativeGetCryptoKeySlots(key); + slots[kSlotAlgorithm] = cloneInternalAlgorithm(slots[kSlotAlgorithm]); + key.#slots = slots; + return slots; + }; + } + } + // Hide NativeCryptoKey from user code. + InternalCryptoKey.prototype.constructor = CryptoKey; + ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); + + ObjectDefineProperties(CryptoKey.prototype, { + type: kEnumerableProperty, + extractable: kEnumerableProperty, + algorithm: kEnumerableProperty, + usages: kEnumerableProperty, + [SymbolToStringTag]: { __proto__: null, - value: keyUsages, - enumerable: false, - configurable: false, - writable: false, + configurable: true, + value: 'CryptoKey', }, }); -} -// All internal code must use new InternalCryptoKey to create -// CryptoKey instances. The CryptoKey class is exposed to end -// user code but is not permitted to be constructed directly. -// Using markTransferMode also allows the CryptoKey to be -// cloned to Workers. -class InternalCryptoKey { - constructor(keyObject, algorithm, keyUsages, extractable) { - markTransferMode(this, true, false); - // When constructed during transfer the properties get assigned - // in the kDeserialize call. - if (keyObject) { - defineCryptoKeyProperties( - this, - keyObject, - algorithm, - extractable, - getSortedUsages(new SafeSet(keyUsages)), - ); + return [CryptoKey, InternalCryptoKey]; +}); + +// The helpers below return a CryptoKey's internal slot value, +// populating the per-instance cache on first access via a single +// native call. The public `type` getter converts the native enum to +// the Web Crypto string. The `usages` helper converts the native usage +// mask to Web Crypto strings. The public `algorithm` / `usages` getters +// on `CryptoKey.prototype` cache their returned objects. + +/** + * Returns the value of a CryptoKey's `[[type]]` internal slot. + * @param {CryptoKey} key + * @returns {'secret' | 'public' | 'private'} + */ +function getCryptoKeyType(key) { + switch (getSlots(key)[kSlotType]) { + case kKeyTypeSecret: return 'secret'; + case kKeyTypePublic: return 'public'; + case kKeyTypePrivate: return 'private'; + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); } } +} + +/** + * Returns the value of a CryptoKey's `[[extractable]]` internal slot. + * @param {CryptoKey} key + * @returns {boolean} + */ +function getCryptoKeyExtractable(key) { + return getSlots(key)[kSlotExtractable]; +} - [kClone]() { - const keyObject = this[kKeyObject]; - const algorithm = this[kAlgorithm]; - const extractable = this[kExtractable]; - const usages = this[kKeyUsages]; +/** + * Returns the CryptoKey's `[[algorithm]]` internal slot, bypassing the + * public `algorithm` getter (which returns a cloned copy). + * @param {CryptoKey} key + * @returns {object} + */ +function getCryptoKeyAlgorithm(key) { + return getSlots(key)[kSlotAlgorithm]; +} - return { - data: { - keyObject, - algorithm, - usages, - extractable, - }, - deserializeInfo: 'internal/crypto/keys:InternalCryptoKey', - }; - } +/** + * Returns the CryptoKey's native `[[usages]]` mask. + * @param {CryptoKey} key + * @returns {number} + */ +function getCryptoKeyUsagesMask(key) { + return getSlots(key)[kSlotUsagesMask]; +} + +/** + * Returns whether a CryptoKey's `[[usages]]` contains `usage`. + * @param {CryptoKey} key + * @param {string} usage + * @returns {boolean} + */ +function hasCryptoKeyUsage(key, usage) { + return hasUsage(getCryptoKeyUsagesMask(key), usage); +} - [kDeserialize]({ keyObject, algorithm, usages, extractable }) { - defineCryptoKeyProperties(this, keyObject, algorithm, extractable, usages); +/** + * Returns the CryptoKey's cached canonical usages array for internal + * consumers, expanding it from the native usage mask on first access. + * @param {Array} slots + * @returns {string[]} + */ +function getCryptoKeyUsagesFromSlots(slots) { + let usages = slots[kSlotUsages]; + if (usages === undefined) { + usages = getUsagesFromMask(slots[kSlotUsagesMask]); + slots[kSlotUsages] = usages; } + return usages; +} + +/** + * Returns the CryptoKey's `[[usages]]` internal slot, bypassing the + * public `usages` getter (which returns a cloned array). The internal + * array is expanded lazily from the native usage mask. + * @param {CryptoKey} key + * @returns {string[]} + */ +function getCryptoKeyUsages(key) { + return getCryptoKeyUsagesFromSlots(getSlots(key)); +} + +/** + * Returns the KeyObjectHandle wrapping the CryptoKey's underlying + * key material. + * @param {CryptoKey} key + * @returns {KeyObjectHandle} + */ +function getCryptoKeyHandle(key) { + return getSlots(key)[kSlotHandle]; } -InternalCryptoKey.prototype.constructor = CryptoKey; -ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); function isCryptoKey(obj) { - return obj != null && obj[kKeyObject] !== undefined; + if (obj == null || typeof obj !== 'object') + return false; + + try { + getSlots(obj); + return true; + } catch { + return false; + } } function importGenericSecretKey( @@ -1146,22 +1202,27 @@ function importGenericSecretKey( 'SyntaxError'); } - let keyObject; + let handle; switch (format) { case 'KeyObject': { - keyObject = keyData; + handle = getKeyObjectHandle(keyData); break; } case 'raw-secret': case 'raw': { - keyObject = createSecretKey(keyData); + handle = new KeyObjectHandle(); + handle.init(kKeyTypeSecret, keyData); break; } default: return undefined; } - return new InternalCryptoKey(keyObject, { name }, usagesSet, false); + return new InternalCryptoKey( + handle, + { name }, + getUsagesMask(usagesSet), + false); } module.exports = { @@ -1184,10 +1245,18 @@ module.exports = { PublicKeyObject, PrivateKeyObject, isKeyObject, + getKeyObjectType, + getKeyObjectHandle, + getKeyObjectSymmetricKeySize, + getKeyObjectAsymmetricKeyType, + getKeyObjectAsymmetricKeyDetails, isCryptoKey, + getCryptoKeyType, + getCryptoKeyExtractable, + getCryptoKeyAlgorithm, + getCryptoKeyUsages, + getCryptoKeyUsagesMask, + hasCryptoKeyUsage, + getCryptoKeyHandle, importGenericSecretKey, - kAlgorithm, - kExtractable, - kKeyType, - kKeyUsages, }; diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 6f842528a82e05..c3418231650b02 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -7,49 +7,40 @@ const { const { HmacJob, - KeyObjectHandle, KmacJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, kSignJobModeSign, kSignJobModeVerify, + SecretKeyGenJob, } = internalBinding('crypto'); const { getBlockSize, + getUsagesMask, hasAnyNotIn, jobPromise, normalizeHashName, - validateKeyOps, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); -const { - generateKey: _generateKey, -} = require('internal/crypto/keygen'); - - -const { - randomBytes: _randomBytes, -} = require('internal/crypto/random'); - -const randomBytes = promisify(_randomBytes); - const { InternalCryptoKey, - SecretKeyObject, - createSecretKey, - kAlgorithm, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getKeyObjectHandle, + getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); -const generateKey = promisify(_generateKey); +const { + importJwkSecretKey, + importSecretKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); -async function hmacGenerateKey(algorithm, extractable, keyUsages) { +function hmacGenerateKey(algorithm, extractable, keyUsages) { const { hash, name, @@ -62,24 +53,21 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) { 'Unsupported key usage for an HMAC key', 'SyntaxError'); } - - let key; - try { - key = await generateKey('hmac', { length }); - } catch (err) { + if (usageSet.size === 0) { throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); } - return new InternalCryptoKey( - key, + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length, hash }, - usageSet, - extractable); + getUsagesMask(usageSet), + extractable)); } -async function kmacGenerateKey(algorithm, extractable, keyUsages) { +function kmacGenerateKey(algorithm, extractable, keyUsages) { const { name, length = { @@ -95,22 +83,18 @@ async function kmacGenerateKey(algorithm, extractable, keyUsages) { `Unsupported key usage for ${name} key`, 'SyntaxError'); } - - let keyData; - try { - keyData = await randomBytes(length / 8); - } catch (err) { + if (usageSet.size === 0) { throw lazyDOMException( - 'The operation failed for an operation-specific reason' + - `[${err.message}]`, - { name: 'OperationError', cause: err }); + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); } - return new InternalCryptoKey( - createSecretKey(keyData), + return jobPromise(() => new SecretKeyGenJob( + kCryptoJobWebCrypto, + length, { name, length }, - usageSet, - extractable); + getUsagesMask(usageSet), + extractable)); } function macImportKey( @@ -127,10 +111,12 @@ function macImportKey( `Unsupported key usage for ${algorithm.name} key`, 'SyntaxError'); } - let keyObject; + let handle; + let length; switch (format) { case 'KeyObject': { - keyObject = keyData; + length = getKeyObjectSymmetricKeySize(keyData) * 8; + handle = getKeyObjectHandle(keyData); break; } case 'raw-secret': @@ -138,31 +124,12 @@ function macImportKey( if (format === 'raw' && !isHmac) { return undefined; } - keyObject = createSecretKey(keyData); + length = keyData.byteLength * 8; + handle = importSecretKey(keyData); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - - if (keyData.kty !== 'oct') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); - - if (usagesSet.size > 0 && - keyData.use !== undefined && - keyData.use !== 'sig') { - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } + validateJwk(keyData, 'oct', extractable, usagesSet, 'sig'); if (keyData.alg !== undefined) { const expected = isHmac ? @@ -174,22 +141,14 @@ function macImportKey( 'DataError'); } - const handle = new KeyObjectHandle(); - try { - handle.initJwk(keyData); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - keyObject = new SecretKeyObject(handle); + handle = importJwkSecretKey(keyData); + length = handle.getSymmetricKeySize() * 8; break; } default: return undefined; } - const { length } = keyObject[kHandle].keyDetail({}); - if (length === 0) throw lazyDOMException('Zero-length key is not supported', 'DataError'); @@ -208,19 +167,19 @@ function macImportKey( } return new InternalCryptoKey( - keyObject, + handle, algorithmObject, - usagesSet, + getUsagesMask(usagesSet), extractable); } function hmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new HmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - normalizeHashName(key[kAlgorithm].hash.name), - key[kKeyObject][kHandle], + normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), + getCryptoKeyHandle(key), data, signature)); } @@ -228,9 +187,9 @@ function hmacSignVerify(key, data, algorithm, signature) { function kmacSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; return jobPromise(() => new KmacJob( - kCryptoJobAsync, + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), algorithm.name, algorithm.customization, algorithm.outputLength / 8, diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index caffa14c5b0d59..07148e7bf1a15f 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -11,54 +11,48 @@ const { const { Buffer } = require('buffer'); const { - KeyObjectHandle, SignJob, - kCryptoJobAsync, - kKeyTypePrivate, - kKeyTypePublic, + kCryptoJobWebCrypto, + kKeyFormatDER, + kKeyFormatRawPublic, + kKeyFormatRawSeed, kSignJobModeSign, kSignJobModeVerify, - kKeyFormatDER, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ML_DSA_44, + EVP_PKEY_ML_DSA_65, + EVP_PKEY_ML_DSA_87, } = internalBinding('crypto'); const { - codes: { - ERR_CRYPTO_INVALID_JWK, - }, -} = require('internal/errors'); - -const { + getUsagesMask, getUsagesUnion, hasAnyNotIn, jobPromise, - validateKeyOps, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectHandle, + getKeyObjectType, InternalCryptoKey, - PrivateKeyObject, - PublicKeyObject, - createPrivateKey, - createPublicKey, - kAlgorithm, - kKeyType, } = require('internal/crypto/keys'); -const generateKeyPair = promisify(_generateKeyPair); +const { + importDerKey, + importJwkKey, + importRawKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { const checkSet = isPublic ? ['verify'] : ['sign']; @@ -69,17 +63,7 @@ function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { } } -function createMlDsaRawKey(name, keyData, isPublic) { - const handle = new KeyObjectHandle(); - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initPqcRaw(name, keyData, keyType)) { - throw lazyDOMException('Invalid keyData', 'DataError'); - } - - return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle); -} - -async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { +function mlDsaGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -89,59 +73,54 @@ async function mlDsaGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyPair; - try { - keyPair = await generateKeyPair(name.toLowerCase()); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const nid = { + '__proto__': null, + 'ML-DSA-44': EVP_PKEY_ML_DSA_44, + 'ML-DSA-65': EVP_PKEY_ML_DSA_65, + 'ML-DSA-87': EVP_PKEY_ML_DSA_87, + }[name]; const publicUsages = getUsagesUnion(usageSet, 'verify'); const privateUsages = getUsagesUnion(usageSet, 'sign'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - keyPair.publicKey, - keyAlgorithm, - publicUsages, - true); - - const privateKey = - new InternalCryptoKey( - keyPair.privateKey, - keyAlgorithm, - privateUsages, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlDsaExportKey(key, format) { try { + const handle = getCryptoKeyHandle(key); switch (format) { case kWebCryptoKeyFormatRaw: { - if (key[kKeyType] === 'private') { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawSeed()); - } - - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPublicKey()); + return TypedArrayPrototypeGetBuffer( + getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey()); } case kWebCryptoKeyFormatSPKI: { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const seed = key[kKeyObject][kHandle].rawSeed(); + const seed = handle.rawSeed(); const buffer = new Uint8Array(54); const orc = { '__proto__': null, 'ML-DSA-44': 0x11, 'ML-DSA-65': 0x12, 'ML-DSA-87': 0x13, - }[key[kAlgorithm].name]; + }[getCryptoKeyAlgorithm(key).name]; TypedArrayPrototypeSet(buffer, [ 0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, @@ -168,26 +147,18 @@ function mlDsaImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { - verifyAcceptableMlDsaKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + verifyAcceptableMlDsaKeyUse( + name, getKeyObjectType(keyData) === 'public', usagesSet); + handle = getKeyObjectHandle(keyData); break; } case 'spki': { verifyAcceptableMlDsaKeyUse(name, true, usagesSet); - try { - keyObject = createPublicKey({ - key: keyData, - format: 'der', - type: 'spki', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, true); break; } case 'pkcs8': { @@ -205,72 +176,24 @@ function mlDsaImportKey( 'NotSupportedError'); } - try { - keyObject = createPrivateKey({ - key: keyData, - format: 'der', - type: 'pkcs8', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, false); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - if (keyData.kty !== 'AKP') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + 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; - - if (usagesSet.size > 0 && keyData.use !== undefined) { - if (keyData.use !== 'sig') - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } - - if (!isPublic && typeof keyData.pub !== 'string') { - throw lazyDOMException('Invalid JWK', 'DataError'); - } - - verifyAcceptableMlDsaKeyUse( - name, - isPublic, - usagesSet); - - try { - const publicKeyObject = createMlDsaRawKey( - name, - Buffer.from(keyData.pub, 'base64url'), - true); - if (isPublic) { - keyObject = publicKeyObject; - } else { - keyObject = createMlDsaRawKey( - name, - Buffer.from(keyData.priv, 'base64url'), - false); + const isPublic = keyData.priv === undefined; + verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + handle = importJwkKey(isPublic, keyData); - if (!createPublicKey(keyObject).equals(publicKeyObject)) { - throw new ERR_CRYPTO_INVALID_JWK(); - } - } - } catch (err) { - throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); + if (!isPublic) { + const publicKey = Buffer.from(keyData.pub, 'base64url'); + if (!Buffer.from(handle.rawPublicKey()).equals(publicKey)) + throw lazyDOMException('Invalid keyData', 'DataError'); } break; } @@ -278,40 +201,36 @@ function mlDsaImportKey( case 'raw-seed': { const isPublic = format === 'raw-public'; verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); - - try { - keyObject = createMlDsaRawKey(name, keyData, isPublic); - } catch (err) { - throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, - usagesSet, + getUsagesMask(usagesSet), extractable); } -async function mlDsaSignVerify(key, data, algorithm, signature) { +function mlDsaSignVerify(key, data, algorithm, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => new SignJob( - kCryptoJobAsync, + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), + undefined, undefined, undefined, undefined, diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 430f0648cd43c5..6edb78d14ba059 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -1,7 +1,6 @@ 'use strict'; const { - PromiseWithResolvers, SafeSet, StringPrototypeToLowerCase, TypedArrayPrototypeGetBuffer, @@ -10,47 +9,49 @@ const { } = primordials; const { - kCryptoJobAsync, + kCryptoJobWebCrypto, KEMDecapsulateJob, KEMEncapsulateJob, - KeyObjectHandle, kKeyFormatDER, - kKeyTypePrivate, - kKeyTypePublic, + kKeyFormatRawPrivate, + kKeyFormatRawPublic, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, + NidKeyPairGenJob, + EVP_PKEY_ML_KEM_512, + EVP_PKEY_ML_KEM_768, + EVP_PKEY_ML_KEM_1024, } = internalBinding('crypto'); const { + getUsagesMask, getUsagesUnion, hasAnyNotIn, - kHandle, - kKeyObject, + jobPromise, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const { + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectHandle, + getKeyObjectType, InternalCryptoKey, - PrivateKeyObject, - PublicKeyObject, - createPrivateKey, - createPublicKey, - kAlgorithm, - kKeyType, } = require('internal/crypto/keys'); -const generateKeyPair = promisify(_generateKeyPair); +const { + importDerKey, + importJwkKey, + importRawKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); -async function mlKemGenerateKey(algorithm, extractable, keyUsages) { +function mlKemGenerateKey(algorithm, extractable, keyUsages) { const { name } = algorithm; const usageSet = new SafeSet(keyUsages); @@ -60,59 +61,56 @@ async function mlKemGenerateKey(algorithm, extractable, keyUsages) { 'SyntaxError'); } - let keyPair; - try { - keyPair = await generateKeyPair(name.toLowerCase()); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + const nid = { + '__proto__': null, + 'ML-KEM-512': EVP_PKEY_ML_KEM_512, + 'ML-KEM-768': EVP_PKEY_ML_KEM_768, + 'ML-KEM-1024': EVP_PKEY_ML_KEM_1024, + }[name]; - const publicUsages = getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); - const privateUsages = getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); + const publicUsages = + getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); + const privateUsages = + getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); const keyAlgorithm = { name }; - const publicKey = - new InternalCryptoKey( - keyPair.publicKey, - keyAlgorithm, - publicUsages, - true); - - const privateKey = - new InternalCryptoKey( - keyPair.privateKey, - keyAlgorithm, - privateUsages, - extractable); + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } - return { __proto__: null, privateKey, publicKey }; + return jobPromise(() => new NidKeyPairGenJob( + kCryptoJobWebCrypto, + nid, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function mlKemExportKey(key, format) { try { + const handle = getCryptoKeyHandle(key); switch (format) { case kWebCryptoKeyFormatRaw: { - if (key[kKeyType] === 'private') { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawSeed()); - } - - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPublicKey()); + return TypedArrayPrototypeGetBuffer( + getCryptoKeyType(key) === 'private' ? handle.rawSeed() : handle.rawPublicKey()); } case kWebCryptoKeyFormatSPKI: { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + return TypedArrayPrototypeGetBuffer( + handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); } case kWebCryptoKeyFormatPKCS8: { - const seed = key[kKeyObject][kHandle].rawSeed(); + const seed = handle.rawSeed(); const buffer = new Uint8Array(86); const orc = { '__proto__': null, 'ML-KEM-512': 0x01, 'ML-KEM-768': 0x02, 'ML-KEM-1024': 0x03, - }[key[kAlgorithm].name]; + }[getCryptoKeyAlgorithm(key).name]; TypedArrayPrototypeSet(buffer, [ 0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, @@ -140,16 +138,6 @@ function verifyAcceptableMlKemKeyUse(name, isPublic, usages) { } } -function createMlKemRawKey(name, keyData, isPublic) { - const handle = new KeyObjectHandle(); - const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; - if (!handle.initPqcRaw(name, keyData, keyType)) { - throw lazyDOMException('Invalid keyData', 'DataError'); - } - - return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle); -} - function mlKemImportKey( format, keyData, @@ -158,26 +146,18 @@ function mlKemImportKey( keyUsages) { const { name } = algorithm; - let keyObject; + let handle; const usagesSet = new SafeSet(keyUsages); switch (format) { case 'KeyObject': { - verifyAcceptableMlKemKeyUse(name, keyData.type === 'public', usagesSet); - keyObject = keyData; + verifyAcceptableMlKemKeyUse( + name, getKeyObjectType(keyData) === 'public', usagesSet); + handle = getKeyObjectHandle(keyData); break; } case 'spki': { verifyAcceptableMlKemKeyUse(name, true, usagesSet); - try { - keyObject = createPublicKey({ - key: keyData, - format: 'der', - type: 'spki', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, true); break; } case 'pkcs8': { @@ -195,105 +175,70 @@ function mlKemImportKey( 'NotSupportedError'); } - try { - keyObject = createPrivateKey({ - key: keyData, - format: 'der', - type: 'pkcs8', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, false); break; } case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name); + break; + } + case 'jwk': { + validateJwk(keyData, 'AKP', extractable, usagesSet, 'enc'); - try { - keyObject = createMlKemRawKey(name, keyData, isPublic); - } catch (err) { - throw lazyDOMException('Invalid keyData', { name: 'DataError', cause: err }); - } + if (keyData.alg !== name) + throw lazyDOMException( + 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); + + const isPublic = keyData.priv === undefined; + verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); + handle = importJwkKey(isPublic, keyData); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== StringPrototypeToLowerCase(name)) { + if (handle.getAsymmetricKeyType() !== StringPrototypeToLowerCase(name)) { throw lazyDOMException('Invalid key type', 'DataError'); } return new InternalCryptoKey( - keyObject, + handle, { name }, - usagesSet, + getUsagesMask(usagesSet), extractable); } function mlKemEncapsulate(encapsulationKey) { - if (encapsulationKey[kKeyType] !== 'public') { + if (getCryptoKeyType(encapsulationKey) !== 'public') { throw lazyDOMException(`Key must be a public key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMEncapsulateJob( - kCryptoJobAsync, - encapsulationKey[kKeyObject][kHandle], + return jobPromise(() => new KEMEncapsulateJob( + kCryptoJobWebCrypto, + getCryptoKeyHandle(encapsulationKey), undefined, undefined, - undefined); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - const { 0: sharedKey, 1: ciphertext } = result; - - resolve({ - sharedKey: TypedArrayPrototypeGetBuffer(sharedKey), - ciphertext: TypedArrayPrototypeGetBuffer(ciphertext), - }); - } - }; - job.run(); - - return promise; + undefined, + undefined)); } function mlKemDecapsulate(decapsulationKey, ciphertext) { - if (decapsulationKey[kKeyType] !== 'private') { + if (getCryptoKeyType(decapsulationKey) !== 'private') { throw lazyDOMException(`Key must be a private key`, 'InvalidAccessError'); } - const { promise, resolve, reject } = PromiseWithResolvers(); - - const job = new KEMDecapsulateJob( - kCryptoJobAsync, - decapsulationKey[kKeyObject][kHandle], + return jobPromise(() => new KEMDecapsulateJob( + kCryptoJobWebCrypto, + getCryptoKeyHandle(decapsulationKey), undefined, undefined, undefined, - ciphertext); - - job.ondone = (error, result) => { - if (error) { - reject(lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: error })); - } else { - resolve(TypedArrayPrototypeGetBuffer(result)); - } - }; - job.run(); - - return promise; + undefined, + ciphertext)); } module.exports = { diff --git a/lib/internal/crypto/pbkdf2.js b/lib/internal/crypto/pbkdf2.js index 2b11535edb3ec7..87f763da9ba8a3 100644 --- a/lib/internal/crypto/pbkdf2.js +++ b/lib/internal/crypto/pbkdf2.js @@ -3,7 +3,7 @@ const { ArrayBuffer, FunctionPrototypeCall, - TypedArrayPrototypeGetBuffer, + PromiseResolve, } = primordials; const { Buffer } = require('buffer'); @@ -12,6 +12,7 @@ const { PBKDF2Job, kCryptoJobAsync, kCryptoJobSync, + kCryptoJobWebCrypto, } = internalBinding('crypto'); const { @@ -23,14 +24,17 @@ const { const { getArrayBufferOrView, normalizeHashName, - kKeyObject, + jobPromise, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); + function pbkdf2(password, salt, iterations, keylen, digest, callback) { if (typeof digest === 'function') { callback = digest; @@ -92,7 +96,6 @@ function check(password, salt, iterations, keylen, digest) { return { password, salt, iterations, keylen, digest }; } -const pbkdf2Promise = promisify(pbkdf2); function validatePbkdf2DeriveBitsLength(length) { if (length === null) throw lazyDOMException('length cannot be null', 'OperationError'); @@ -104,25 +107,20 @@ function validatePbkdf2DeriveBitsLength(length) { } } -async function pbkdf2DeriveBits(algorithm, baseKey, length) { +function pbkdf2DeriveBits(algorithm, baseKey, length) { validatePbkdf2DeriveBitsLength(length); const { iterations, hash, salt } = algorithm; if (length === 0) - return new ArrayBuffer(0); - - let result; - try { - result = await pbkdf2Promise( - baseKey[kKeyObject].export(), salt, iterations, length / 8, normalizeHashName(hash.name), - ); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } + return PromiseResolve(new ArrayBuffer(0)); - return TypedArrayPrototypeGetBuffer(result); + return jobPromise(() => new PBKDF2Job( + kCryptoJobWebCrypto, + getCryptoKeyHandle(baseKey), + salt, + iterations, + length / 8, + normalizeHashName(hash.name))); } module.exports = { diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index d3eb35a74db47b..d72d55c2bbff42 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -3,22 +3,23 @@ const { MathCeil, SafeSet, + TypedArrayPrototypeGetBuffer, Uint8Array, } = primordials; const { - KeyObjectHandle, RSACipherJob, - RSAKeyExportJob, SignJob, - kCryptoJobAsync, + kCryptoJobWebCrypto, + kKeyFormatDER, kSignJobModeSign, kSignJobModeVerify, - kKeyVariantRSA_SSA_PKCS1_v1_5, - kKeyVariantRSA_PSS, kKeyVariantRSA_OAEP, - kKeyTypePrivate, + kKeyVariantRSA_SSA_PKCS1_v1_5, kWebCryptoCipherEncrypt, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, + RsaKeyPairGenJob, RSA_PKCS1_PSS_PADDING, } = internalBinding('crypto'); @@ -29,41 +30,32 @@ const { const { bigIntArrayToUnsignedInt, getDigestSizeInBytes, + getUsagesMask, getUsagesUnion, hasAnyNotIn, jobPromise, normalizeHashName, - validateKeyOps, validateMaxBufferLength, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { lazyDOMException, - promisify, } = require('internal/util'); const { InternalCryptoKey, - PrivateKeyObject, - PublicKeyObject, - createPublicKey, - createPrivateKey, - kAlgorithm, - kKeyType, + getCryptoKeyAlgorithm, + getCryptoKeyHandle, + getCryptoKeyType, + getKeyObjectHandle, + getKeyObjectType, } = require('internal/crypto/keys'); const { - generateKeyPair: _generateKeyPair, -} = require('internal/crypto/keygen'); - -const kRsaVariants = { - 'RSASSA-PKCS1-v1_5': kKeyVariantRSA_SSA_PKCS1_v1_5, - 'RSA-PSS': kKeyVariantRSA_PSS, - 'RSA-OAEP': kKeyVariantRSA_OAEP, -}; -const generateKeyPair = promisify(_generateKeyPair); + importDerKey, + importJwkKey, + validateJwk, +} = require('internal/crypto/webcrypto_util'); function verifyAcceptableRsaKeyUse(name, isPublic, usages) { let checkSet; @@ -93,27 +85,27 @@ function validateRsaOaepAlgorithm(algorithm) { } } -async function rsaOaepCipher(mode, key, data, algorithm) { +function rsaOaepCipher(mode, key, data, algorithm) { validateRsaOaepAlgorithm(algorithm); const type = mode === kWebCryptoCipherEncrypt ? 'public' : 'private'; - if (key[kKeyType] !== type) { + if (getCryptoKeyType(key) !== type) { throw lazyDOMException( 'The requested operation is not valid for the provided key', 'InvalidAccessError'); } - return await jobPromise(() => new RSACipherJob( - kCryptoJobAsync, + return jobPromise(() => new RSACipherJob( + kCryptoJobWebCrypto, mode, - key[kKeyObject][kHandle], + getCryptoKeyHandle(key), data, kKeyVariantRSA_OAEP, - normalizeHashName(key[kAlgorithm].hash.name), + normalizeHashName(getCryptoKeyAlgorithm(key).hash.name), algorithm.label)); } -async function rsaKeyGenerate( +function rsaKeyGenerate( algorithm, extractable, keyUsages, @@ -150,18 +142,6 @@ async function rsaKeyGenerate( } } - let keyPair; - try { - keyPair = await generateKeyPair('rsa', { - modulusLength, - publicExponent: publicExponentConverted, - }); - } catch (err) { - throw lazyDOMException( - 'The operation failed for an operation-specific reason', - { name: 'OperationError', cause: err }); - } - const keyAlgorithm = { name, modulusLength, @@ -169,6 +149,12 @@ async function rsaKeyGenerate( hash, }; + if (publicExponentConverted < 3 || publicExponentConverted % 2 === 0) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + 'OperationError'); + } + let publicUsages; let privateUsages; switch (name) { @@ -184,29 +170,42 @@ async function rsaKeyGenerate( } } - const publicKey = - new InternalCryptoKey( - keyPair.publicKey, - keyAlgorithm, - publicUsages, - true); - - const privateKey = - new InternalCryptoKey( - keyPair.privateKey, - keyAlgorithm, - privateUsages, - extractable); - - return { __proto__: null, publicKey, privateKey }; + if (privateUsages.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + + return jobPromise(() => new RsaKeyPairGenJob( + kCryptoJobWebCrypto, + kKeyVariantRSA_SSA_PKCS1_v1_5, + modulusLength, + publicExponentConverted, + keyAlgorithm, + getUsagesMask(publicUsages), + getUsagesMask(privateUsages), + extractable)); } function rsaExportKey(key, format) { - return jobPromise(() => new RSAKeyExportJob( - kCryptoJobAsync, - format, - key[kKeyObject][kHandle], - kRsaVariants[key[kAlgorithm].name])); + try { + switch (format) { + case kWebCryptoKeyFormatSPKI: { + return TypedArrayPrototypeGetBuffer( + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatSPKI)); + } + case kWebCryptoKeyFormatPKCS8: { + return TypedArrayPrototypeGetBuffer( + getCryptoKeyHandle(key).export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null)); + } + default: + return undefined; + } + } catch (err) { + throw lazyDOMException( + 'The operation failed for an operation-specific reason', + { name: 'OperationError', cause: err }); + } } function rsaImportKey( @@ -216,68 +215,27 @@ function rsaImportKey( extractable, keyUsages) { const usagesSet = new SafeSet(keyUsages); - let keyObject; + let handle; switch (format) { case 'KeyObject': { - verifyAcceptableRsaKeyUse(algorithm.name, keyData.type === 'public', usagesSet); - keyObject = keyData; + verifyAcceptableRsaKeyUse( + algorithm.name, getKeyObjectType(keyData) === 'public', usagesSet); + handle = getKeyObjectHandle(keyData); break; } case 'spki': { verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet); - try { - keyObject = createPublicKey({ - key: keyData, - format: 'der', - type: 'spki', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, true); break; } case 'pkcs8': { verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet); - try { - keyObject = createPrivateKey({ - key: keyData, - format: 'der', - type: 'pkcs8', - }); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } + handle = importDerKey(keyData, false); break; } case 'jwk': { - if (!keyData.kty) - throw lazyDOMException('Invalid keyData', 'DataError'); - - if (keyData.kty !== 'RSA') - throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); - - verifyAcceptableRsaKeyUse( - algorithm.name, - keyData.d === undefined, - usagesSet); - - if (usagesSet.size > 0 && keyData.use !== undefined) { - const checkUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig'; - if (keyData.use !== checkUse) - throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); - } - - validateKeyOps(keyData.key_ops, usagesSet); - - if (keyData.ext !== undefined && - keyData.ext === false && - extractable === true) { - throw lazyDOMException( - 'JWK "ext" Parameter and extractable mismatch', - 'DataError'); - } + const expectedUse = algorithm.name === 'RSA-OAEP' ? 'enc' : 'sig'; + validateJwk(keyData, 'RSA', extractable, usagesSet, expectedUse); if (keyData.alg !== undefined) { const expected = @@ -292,75 +250,70 @@ function rsaImportKey( 'DataError'); } - const handle = new KeyObjectHandle(); - let type; - try { - type = handle.initJwk(keyData); - } catch (err) { - throw lazyDOMException( - 'Invalid keyData', { name: 'DataError', cause: err }); - } - if (type === undefined) - throw lazyDOMException('Invalid keyData', 'DataError'); - - keyObject = type === kKeyTypePrivate ? - new PrivateKeyObject(handle) : - new PublicKeyObject(handle); - + const isPublic = keyData.d === undefined; + verifyAcceptableRsaKeyUse(algorithm.name, isPublic, usagesSet); + handle = importJwkKey(isPublic, keyData); break; } default: return undefined; } - if (keyObject.asymmetricKeyType !== 'rsa') { + if (handle.getAsymmetricKeyType() !== 'rsa') { throw lazyDOMException('Invalid key type', 'DataError'); } const { modulusLength, publicExponent, - } = keyObject[kHandle].keyDetail({}); + } = handle.keyDetail({}); - return new InternalCryptoKey(keyObject, { + return new InternalCryptoKey(handle, { name: algorithm.name, modulusLength, publicExponent: new Uint8Array(publicExponent), hash: algorithm.hash, - }, usagesSet, extractable); + }, getUsagesMask(usagesSet), extractable); } -async function rsaSignVerify(key, data, { saltLength }, signature) { +function rsaSignVerify(key, data, { saltLength }, signature) { const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; const type = mode === kSignJobModeSign ? 'private' : 'public'; - if (key[kKeyType] !== type) + if (getCryptoKeyType(key) !== type) throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - return await jobPromise(() => { - if (key[kAlgorithm].name === 'RSA-PSS') { + const algorithm = getCryptoKeyAlgorithm(key); + if (algorithm.name === 'RSA-PSS') { + try { validateInt32( saltLength, 'algorithm.saltLength', 0, - MathCeil((key[kAlgorithm].modulusLength - 1) / 8) - getDigestSizeInBytes(key[kAlgorithm].hash.name) - 2); + 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 }); } + } - return new SignJob( - kCryptoJobAsync, - signature === undefined ? kSignJobModeSign : kSignJobModeVerify, - key[kKeyObject][kHandle], - undefined, - undefined, - undefined, - data, - normalizeHashName(key[kAlgorithm].hash.name), - saltLength, - key[kAlgorithm].name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, - undefined, - undefined, - signature); - }); + return jobPromise(() => new SignJob( + kCryptoJobWebCrypto, + signature === undefined ? kSignJobModeSign : kSignJobModeVerify, + getCryptoKeyHandle(key), + undefined, + undefined, + undefined, + undefined, + data, + normalizeHashName(algorithm.hash.name), + saltLength, + algorithm.name === 'RSA-PSS' ? RSA_PKCS1_PSS_PADDING : undefined, + undefined, + undefined, + signature)); } diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 2d9c9a7ae023fe..3830ecc0b128d2 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -130,20 +130,23 @@ function getIntOption(name, options) { return undefined; } -Sign.prototype.sign = function sign(options, encoding) { - if (!options) +Sign.prototype.sign = function sign(privateKey, encoding) { + if (!privateKey) throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); - const { data, format, type, passphrase } = preparePrivateKey(options, true); + const { data, format, type, passphrase, namedCurve } = + preparePrivateKey(privateKey, 'privateKey'); // Options specific to RSA - const rsaPadding = getPadding(options); - const pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(privateKey); + const pssSaltLength = getSaltLength(privateKey); // Options specific to (EC)DSA - const dsaSigEnc = getDSASignatureEncoding(options); + const dsaSigEnc = getDSASignatureEncoding(privateKey); - const ret = this[kHandle].sign(data, format, type, passphrase, rsaPadding, + const ret = this[kHandle].sign(data, format, type, + passphrase, namedCurve, + rsaPadding, pssSaltLength, dsaSigEnc); if (encoding && encoding !== 'buffer') @@ -179,6 +182,7 @@ function signOneShot(algorithm, data, key, callback) { format: keyFormat, type: keyType, passphrase: keyPassphrase, + namedCurve: keyNamedCurve, } = preparePrivateKey(key); const job = new SignJob( @@ -188,6 +192,7 @@ function signOneShot(algorithm, data, key, callback) { keyFormat, keyType, keyPassphrase, + keyNamedCurve, data, algorithm, pssSaltLength, @@ -227,24 +232,27 @@ ObjectSetPrototypeOf(Verify, Writable); Verify.prototype._write = Sign.prototype._write; Verify.prototype.update = Sign.prototype.update; -Verify.prototype.verify = function verify(options, signature, sigEncoding) { +Verify.prototype.verify = function verify(key, signature, sigEncoding) { const { data, format, type, passphrase, - } = preparePublicOrPrivateKey(options, true); + namedCurve, + } = preparePublicOrPrivateKey(key, 'key'); // Options specific to RSA - const rsaPadding = getPadding(options); - const pssSaltLength = getSaltLength(options); + const rsaPadding = getPadding(key); + const pssSaltLength = getSaltLength(key); // Options specific to (EC)DSA - const dsaSigEnc = getDSASignatureEncoding(options); + const dsaSigEnc = getDSASignatureEncoding(key); signature = getArrayBufferOrView(signature, 'signature', sigEncoding); - return this[kHandle].verify(data, format, type, passphrase, signature, + return this[kHandle].verify(data, format, type, + passphrase, namedCurve, + signature, rsaPadding, pssSaltLength, dsaSigEnc); }; @@ -274,6 +282,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) { format: keyFormat, type: keyType, passphrase: keyPassphrase, + namedCurve: keyNamedCurve, } = preparePublicOrPrivateKey(key); const job = new SignJob( @@ -283,6 +292,7 @@ function verifyOneShot(algorithm, data, key, signature, callback) { keyFormat, keyType, keyPassphrase, + keyNamedCurve, data, algorithm, pssSaltLength, diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index ebd607aea1f585..9fca6b3864aca0 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -3,19 +3,19 @@ const { ArrayBufferIsView, ArrayBufferPrototypeGetByteLength, - ArrayFrom, ArrayPrototypeIncludes, ArrayPrototypePush, BigInt, DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, - FunctionPrototypeBind, Number, ObjectDefineProperty, ObjectEntries, ObjectKeys, ObjectPrototypeHasOwnProperty, + PromisePrototypeThen, + PromiseReject, PromiseWithResolvers, SafeSet, StringPrototypeToUpperCase, @@ -78,6 +78,7 @@ const { emitExperimentalWarning, filterDuplicateStrings, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -91,10 +92,10 @@ const { isDataView, isArrayBufferView, isAnyArrayBuffer, + isPromise, } = require('internal/util/types'); const kHandle = Symbol('kHandle'); -const kKeyObject = Symbol('kKeyObject'); // This is here because many functions accepted binary strings without // any explicit encoding in older versions of node, and we don't want @@ -246,6 +247,10 @@ const kAlgorithmDefinitions = { }, 'cSHAKE128': { 'digest': 'CShakeParams' }, 'cSHAKE256': { 'digest': 'CShakeParams' }, + 'KT128': { 'digest': 'KangarooTwelveParams' }, + 'KT256': { 'digest': 'KangarooTwelveParams' }, + 'TurboSHAKE128': { 'digest': 'TurboShakeParams' }, + 'TurboSHAKE256': { 'digest': 'TurboShakeParams' }, 'ECDH': { 'generateKey': 'EcKeyGenParams', 'exportKey': null, @@ -393,12 +398,11 @@ const kAlgorithmDefinitions = { // Conditionally supported algorithms const conditionalAlgorithms = { - 'AES-KW': !process.features.openssl_is_boringssl, 'AES-OCB': !!hasAesOcbMode, 'Argon2d': !!Argon2Job, 'Argon2i': !!Argon2Job, 'Argon2id': !!Argon2Job, - 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || + 'ChaCha20-Poly1305': process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getHashes(), 'shake128'), @@ -443,6 +447,10 @@ const experimentalAlgorithms = [ 'SHA3-256', 'SHA3-384', 'SHA3-512', + 'TurboSHAKE128', + 'TurboSHAKE256', + 'KT128', + 'KT256', 'X448', ]; @@ -515,12 +523,16 @@ const simpleAlgorithmDictionaries = { KmacParams: { customization: 'BufferSource', }, + KangarooTwelveParams: { + customization: 'BufferSource', + }, + TurboShakeParams: {}, }; -function validateMaxBufferLength(data, name) { - if (data.byteLength > kMaxBufferLength) { +function validateMaxBufferLength(data, name, max = kMaxBufferLength) { + if (data.byteLength > max) { throw lazyDOMException( - `${name} must be less than ${kMaxBufferLength + 1} bits`, + `${name} must be at most ${max} bytes`, 'OperationError'); } } @@ -648,25 +660,80 @@ const validateByteSource = hideStackFrames((val, name) => { val); }); -function onDone(resolve, reject, err, result) { - if (err) { - return reject(lazyDOMException( +// CryptoJob constructors can synchronously throw while running their native +// AdditionalConfig hook. WebCrypto needs those operation-specific setup +// failures to reject with an OperationError. +function jobPromise(getJob) { + try { + return getJob().run(); + } catch (err) { + return PromiseReject(lazyDOMException( 'The operation failed for an operation-specific reason', { name: 'OperationError', cause: err })); } - resolve(result); } -function jobPromise(getJob) { - const { promise, resolve, reject } = PromiseWithResolvers(); +// Temporarily shadow inherited then accessors on WebCrypto result objects. +// Promise resolution reads "then" synchronously for thenable assimilation. +// Returning an own undefined data property keeps that lookup from reaching +// user-mutated prototypes. +function prepareWebCryptoResult(value) { + if ((value === null || typeof value !== 'object') && + typeof value !== 'function') { + return false; + } + if (isPromise(value) || ObjectPrototypeHasOwnProperty(value, 'then')) + return false; + setOwnProperty(value, 'then', undefined); + return true; +} + +// Remove the temporary then property installed by prepareWebCryptoResult(). +function cleanupWebCryptoResult(value) { + delete value.then; +} + +// Resolve a WebCrypto promise while inherited then accessors are shadowed. +function resolveWebCryptoResult(resolve, value) { + const shouldCleanupResult = prepareWebCryptoResult(value); try { - const job = getJob(); - job.ondone = FunctionPrototypeBind(onDone, job, resolve, reject); - job.run(); + resolve(value); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(value); + } +} + +// Run a WebCrypto promise reaction and settle the outer promise. +function settleJobPromise(handler, resolve, reject, value, isRejected) { + try { + if (typeof handler === 'function') { + resolveWebCryptoResult(resolve, handler(value)); + } else if (isRejected) { + reject(value); + } else { + resolveWebCryptoResult(resolve, value); + } } catch (err) { - onDone(resolve, reject, err); + reject(err); } - return promise; +} + +// Promise.prototype.then gets promise.constructor to determine the result +// promise's species. These promises are internal WebCrypto intermediates, so +// make that lookup stay on the promise itself instead of user-mutated state. +function jobPromiseThen(promise, onFulfilled, onRejected) { + const { + promise: resultPromise, + resolve, + reject, + } = PromiseWithResolvers(); + setOwnProperty(promise, 'constructor', undefined); + PromisePrototypeThen( + promise, + (value) => settleJobPromise(onFulfilled, resolve, reject, value, false), + (value) => settleJobPromise(onRejected, resolve, reject, value, true)); + return resultPromise; } // In WebCrypto, the publicExponent option in RSA is represented as a @@ -707,6 +774,12 @@ function getStringOption(options, key) { return value; } +/** + * Returns the requested usages that are present in `usageSet`. + * @param {SafeSet} usageSet + * @param {...string} usages + * @returns {SafeSet} + */ function getUsagesUnion(usageSet, ...usages) { const newset = new SafeSet(); for (let n = 0; n < usages.length; n++) { @@ -716,28 +789,76 @@ function getUsagesUnion(usageSet, ...usages) { return newset; } -const kCanonicalUsageOrder = new SafeSet([ +// Must be at most 31 entries. +const kCanonicalUsageOrder = [ 'encrypt', 'decrypt', 'sign', 'verify', 'deriveKey', 'deriveBits', 'wrapKey', 'unwrapKey', 'encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits', -]); +]; + +const kUsageMasks = { + __proto__: null, +}; +const kUsageByMask = { + __proto__: null, +}; +// Derive both lookup tables from kCanonicalUsageOrder so adding a new +// usage only requires updating the canonical list above. The numeric +// mask uses the usage's canonical index as its bit position. +for (let n = 0; n < kCanonicalUsageOrder.length; n++) { + const usage = kCanonicalUsageOrder[n]; + const mask = 1 << n; + kUsageMasks[usage] = mask; + kUsageByMask[mask] = usage; +} /** - * Returns the usages from `usageSet` as an array in the canonical order - * defined by {@link kCanonicalUsageOrder}. + * Returns a bit mask representing the usages from `usageSet`. * @param {SafeSet} usageSet + * @returns {number} + */ +function getUsagesMask(usageSet) { + // No usages is a valid state for some public keys, represented by a + // zero mask. + if (usageSet.size === 0) return 0; + let mask = 0; + for (const usage of usageSet) { + mask |= kUsageMasks[usage]; + } + return mask; +} + +/** + * Returns whether `mask` contains `usage`. + * @param {number} mask + * @param {string} usage + * @returns {boolean} + */ +function hasUsage(mask, usage) { + return (mask & kUsageMasks[usage]) !== 0; +} + +/** + * Returns the usages represented by `mask` in canonical order. + * @param {number} mask * @returns {string[]} */ -function getSortedUsages(usageSet) { - if (usageSet.size <= 1) { - return ArrayFrom(usageSet); +function getUsagesFromMask(mask) { + // Short circuit most common cases, empty and single usage + // No usages is a valid state for some public keys + if (mask === 0) return []; + // A mask with exactly one bit set maps directly to one usage + if ((mask & (mask - 1)) === 0) { + return [kUsageByMask[mask]]; } + // Multiple usages need to be expanded in canonical order. const result = []; - for (const usage of kCanonicalUsageOrder) { - if (usageSet.has(usage)) ArrayPrototypePush(result, usage); + for (let n = 0; n < kCanonicalUsageOrder.length; n++) { + if (mask & (1 << n)) + ArrayPrototypePush(result, kCanonicalUsageOrder[n]); } return result; } @@ -779,32 +900,20 @@ function getDigestSizeInBytes(name) { } } -const kKeyOps = { - __proto__: null, - sign: 1, - verify: 2, - encrypt: 3, - decrypt: 4, - wrapKey: 5, - unwrapKey: 6, - deriveKey: 7, - deriveBits: 8, -}; - function validateKeyOps(keyOps, usagesSet) { if (keyOps === undefined) return; validateArray(keyOps, 'keyData.key_ops'); - let flags = 0; + let keyOpsMask = 0; for (let n = 0; n < keyOps.length; n++) { const op = keyOps[n]; - const op_flag = kKeyOps[op]; + const opMask = kUsageMasks[op]; // Skipping unknown key ops - if (op_flag === undefined) + if (opMask === undefined) continue; // Have we seen it already? if so, error - if (flags & (1 << op_flag)) + if (keyOpsMask & opMask) throw lazyDOMException('Duplicate key operation', 'DataError'); - flags |= (1 << op_flag); + keyOpsMask |= opMask; // TODO(@jasnell): RFC7517 section 4.3 strong recommends validating // key usage combinations. Specifically, it says that unrelated key @@ -812,12 +921,11 @@ function validateKeyOps(keyOps, usagesSet) { } if (usagesSet !== undefined) { - for (const use of usagesSet) { - if (!ArrayPrototypeIncludes(keyOps, use)) { - throw lazyDOMException( - 'Key operations and usage mismatch', - 'DataError'); - } + const usagesMask = getUsagesMask(usagesSet); + if ((keyOpsMask & usagesMask) !== usagesMask) { + throw lazyDOMException( + 'Key operations and usage mismatch', + 'DataError'); } } } @@ -840,7 +948,6 @@ module.exports = { getDataViewOrTypedArrayBuffer, getHashes, kHandle, - kKeyObject, setEngine, toBuf, @@ -851,6 +958,9 @@ module.exports = { validateByteSource, validateKeyOps, jobPromise, + jobPromiseThen, + cleanupWebCryptoResult, + prepareWebCryptoResult, validateMaxBufferLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, @@ -858,7 +968,9 @@ module.exports = { getDigestSizeInBytes, getStringOption, getUsagesUnion, - getSortedUsages, + getUsagesMask, + getUsagesFromMask, + hasUsage, secureHeapUsed, getCachedHashId, getHashCache, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 685301ea666eb1..6ab66b55486fb9 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1,16 +1,24 @@ 'use strict'; const { - ArrayPrototypeIncludes, + ArrayIsArray, + ArrayPrototypeSlice, FunctionPrototypeCall, JSONParse, JSONStringify, ObjectDefineProperties, + ObjectKeys, + ObjectPrototypeHasOwnProperty, + ObjectSetPrototypeOf, + PromiseReject, + PromiseResolve, ReflectApply, ReflectConstruct, + SafeArrayIterator, StringPrototypeRepeat, StringPrototypeSlice, StringPrototypeStartsWith, + SymbolIterator, SymbolToStringTag, TypedArrayPrototypeGetBuffer, } = primordials; @@ -23,7 +31,10 @@ const { kWebCryptoCipherDecrypt, } = internalBinding('crypto'); -const { TextDecoder, TextEncoder } = require('internal/encoding'); +const { + decodeUTF8, + encodeUtf8String, +} = internalBinding('encoding_binding'); const { codes: { @@ -35,11 +46,15 @@ const { const { createPublicKey, CryptoKey, + getCryptoKeyAlgorithm, + getCryptoKeyExtractable, + getCryptoKeyHandle, + getCryptoKeyType, + getCryptoKeyUsages, + getCryptoKeyUsagesMask, + hasCryptoKeyUsage, importGenericSecretKey, - kAlgorithm, - kKeyUsages, - kExtractable, - kKeyType, + PrivateKeyObject, } = require('internal/crypto/keys'); const { @@ -47,18 +62,20 @@ const { } = require('internal/crypto/hash'); const { + cleanupWebCryptoResult, getBlockSize, + jobPromiseThen, normalizeAlgorithm, normalizeHashName, + prepareWebCryptoResult, validateMaxBufferLength, - kHandle, - kKeyObject, } = require('internal/crypto/util'); const { emitExperimentalWarning, kEnumerableProperty, lazyDOMException, + setOwnProperty, } = require('internal/util'); const { @@ -66,9 +83,38 @@ const { randomUUID: _randomUUID, } = require('internal/crypto/random'); +const { + isPromise, +} = require('internal/util/types'); + let webidl; -async function digest(algorithm, data) { +// WebCrypto methods return promises, including for synchronous validation +// failures. Keep that conversion in one place so method bodies stay readable. +function callSubtleCryptoMethod(fn, receiver, args) { + try { + const result = ReflectApply(fn, receiver, args); + if (isPromise(result)) + return result; + // PromiseResolve() performs thenable assimilation for object results. + // Shadow inherited then accessors while it resolves synchronous results. + const shouldCleanupResult = prepareWebCryptoResult(result); + try { + return PromiseResolve(result); + } finally { + if (shouldCleanupResult) + cleanupWebCryptoResult(result); + } + } catch (err) { + return PromiseReject(err); + } +} + +function digest(algorithm, data) { + return callSubtleCryptoMethod(digestImpl, this, arguments); +} + +function digestImpl(algorithm, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -85,7 +131,7 @@ async function digest(algorithm, data) { algorithm = normalizeAlgorithm(algorithm, 'digest'); - return await FunctionPrototypeCall(asyncDigest, this, algorithm, data); + return FunctionPrototypeCall(asyncDigest, this, algorithm, data); } function randomUUID() { @@ -93,7 +139,14 @@ function randomUUID() { return _randomUUID(); } -async function generateKey( +function generateKey( + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(generateKeyImpl, this, arguments); +} + +function generateKeyImpl( algorithm, extractable, keyUsages) { @@ -116,18 +169,14 @@ async function generateKey( }); algorithm = normalizeAlgorithm(algorithm, 'generateKey'); - let result; - let resultType; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaKeyGenerate(algorithm, extractable, keyUsages); - break; case 'Ed25519': // Fall through case 'Ed448': @@ -135,22 +184,16 @@ async function generateKey( case 'X25519': // Fall through case 'X448': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgGenerateKey(algorithm, extractable, keyUsages); - break; case 'ECDSA': // Fall through case 'ECDH': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecGenerateKey(algorithm, extractable, keyUsages); - break; case 'HMAC': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') + return require('internal/crypto/mac') .hmacGenerateKey(algorithm, extractable, keyUsages); - break; case 'AES-CTR': // Fall through case 'AES-CBC': @@ -160,59 +203,40 @@ async function generateKey( case 'AES-OCB': // Fall through case 'AES-KW': - resultType = 'CryptoKey'; - result = await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesGenerateKey(algorithm, extractable, keyUsages); - break; case 'ChaCha20-Poly1305': - resultType = 'CryptoKey'; - result = await require('internal/crypto/chacha20_poly1305') + return require('internal/crypto/chacha20_poly1305') .c20pGenerateKey(algorithm, extractable, keyUsages); - break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_dsa') + return require('internal/crypto/ml_dsa') .mlDsaGenerateKey(algorithm, extractable, keyUsages); - break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - resultType = 'CryptoKeyPair'; - result = await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemGenerateKey(algorithm, extractable, keyUsages); - break; case 'KMAC128': // Fall through case 'KMAC256': - resultType = 'CryptoKey'; - result = await require('internal/crypto/mac') + return require('internal/crypto/mac') .kmacGenerateKey(algorithm, extractable, keyUsages); - break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } +} - if ( - (resultType === 'CryptoKey' && - (result[kKeyType] === 'secret' || result[kKeyType] === 'private') && - result[kKeyUsages].length === 0) || - (resultType === 'CryptoKeyPair' && result.privateKey[kKeyUsages].length === 0) - ) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } - - return result; +function deriveBits(algorithm, baseKey, length = null) { + return callSubtleCryptoMethod(deriveBitsImpl, this, arguments); } -async function deriveBits(algorithm, baseKey, length = null) { +function deriveBitsImpl(algorithm, baseKey, length = null) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -234,12 +258,12 @@ async function deriveBits(algorithm, baseKey, length = null) { } algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); - if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveBits')) { + if (!hasCryptoKeyUsage(baseKey, 'deriveBits')) { throw lazyDOMException( 'baseKey does not have deriveBits usage', 'InvalidAccessError'); } - if (baseKey[kAlgorithm].name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); switch (algorithm.name) { case 'X25519': @@ -247,20 +271,20 @@ async function deriveBits(algorithm, baseKey, length = null) { case 'X448': // Fall through case 'ECDH': - return await require('internal/crypto/diffiehellman') + return require('internal/crypto/diffiehellman') .ecdhDeriveBits(algorithm, baseKey, length); case 'HKDF': - return await require('internal/crypto/hkdf') + return require('internal/crypto/hkdf') .hkdfDeriveBits(algorithm, baseKey, length); case 'PBKDF2': - return await require('internal/crypto/pbkdf2') + return require('internal/crypto/pbkdf2') .pbkdf2DeriveBits(algorithm, baseKey, length); case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': - return await require('internal/crypto/argon2') + return require('internal/crypto/argon2') .argon2DeriveBits(algorithm, baseKey, length); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); @@ -305,7 +329,16 @@ function getKeyLength({ name, length, hash }) { } } -async function deriveKey( +function deriveKey( + algorithm, + baseKey, + derivedKeyAlgorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(deriveKeyImpl, this, arguments); +} + +function deriveKeyImpl( algorithm, baseKey, derivedKeyAlgorithm, @@ -339,12 +372,12 @@ async function deriveKey( algorithm = normalizeAlgorithm(algorithm, 'deriveBits'); derivedKeyAlgorithm = normalizeAlgorithm(derivedKeyAlgorithm, 'importKey'); - if (!ArrayPrototypeIncludes(baseKey[kKeyUsages], 'deriveKey')) { + if (!hasCryptoKeyUsage(baseKey, 'deriveKey')) { throw lazyDOMException( 'baseKey does not have deriveKey usage', 'InvalidAccessError'); } - if (baseKey[kAlgorithm].name !== algorithm.name) + if (getCryptoKeyAlgorithm(baseKey).name !== algorithm.name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); const length = getKeyLength(normalizeAlgorithm(arguments[2], 'get key length')); @@ -355,15 +388,15 @@ async function deriveKey( case 'X448': // Fall through case 'ECDH': - bits = await require('internal/crypto/diffiehellman') + bits = require('internal/crypto/diffiehellman') .ecdhDeriveBits(algorithm, baseKey, length); break; case 'HKDF': - bits = await require('internal/crypto/hkdf') + bits = require('internal/crypto/hkdf') .hkdfDeriveBits(algorithm, baseKey, length); break; case 'PBKDF2': - bits = await require('internal/crypto/pbkdf2') + bits = require('internal/crypto/pbkdf2') .pbkdf2DeriveBits(algorithm, baseKey, length); break; case 'Argon2d': @@ -371,33 +404,33 @@ async function deriveKey( case 'Argon2i': // Fall through case 'Argon2id': - bits = await require('internal/crypto/argon2') + bits = require('internal/crypto/argon2') .argon2DeriveBits(algorithm, baseKey, length); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return jobPromiseThen(bits, (bits) => FunctionPrototypeCall( importKeySync, this, 'raw-secret', bits, derivedKeyAlgorithm, extractable, keyUsages, - ); + )); } -async function exportKeySpki(key) { - switch (key[kAlgorithm].name) { +function exportKeySpki(key) { + switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaExportKey(key, kWebCryptoKeyFormatSPKI); case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatSPKI); case 'Ed25519': // Fall through @@ -406,14 +439,13 @@ async function exportKeySpki(key) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatSPKI); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatSPKI); case 'ML-KEM-512': @@ -421,7 +453,6 @@ async function exportKeySpki(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatSPKI); default: @@ -429,19 +460,19 @@ async function exportKeySpki(key) { } } -async function exportKeyPkcs8(key) { - switch (key[kAlgorithm].name) { +function exportKeyPkcs8(key) { + switch (getCryptoKeyAlgorithm(key).name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatPKCS8); case 'Ed25519': // Fall through @@ -450,14 +481,13 @@ async function exportKeyPkcs8(key) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatPKCS8); case 'ML-KEM-512': @@ -465,7 +495,6 @@ async function exportKeyPkcs8(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatPKCS8); default: @@ -473,12 +502,12 @@ async function exportKeyPkcs8(key) { } } -async function exportKeyRawPublic(key, format) { - switch (key[kAlgorithm].name) { +function exportKeyRawPublic(key, format) { + switch (getCryptoKeyAlgorithm(key).name) { case 'ECDSA': // Fall through case 'ECDH': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecExportKey(key, kWebCryptoKeyFormatRaw); case 'Ed25519': // Fall through @@ -487,7 +516,7 @@ async function exportKeyRawPublic(key, format) { case 'X25519': // Fall through case 'X448': - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .cfrgExportKey(key, kWebCryptoKeyFormatRaw); case 'ML-DSA-44': // Fall through @@ -498,7 +527,6 @@ async function exportKeyRawPublic(key, format) { if (format !== 'raw-public') { return undefined; } - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); } @@ -511,7 +539,6 @@ async function exportKeyRawPublic(key, format) { if (format !== 'raw-public') { return undefined; } - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatRaw); } @@ -520,14 +547,13 @@ async function exportKeyRawPublic(key, format) { } } -async function exportKeyRawSeed(key) { - switch (key[kAlgorithm].name) { +function exportKeyRawSeed(key) { + switch (getCryptoKeyAlgorithm(key).name) { case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - // Note: mlDsaExportKey does not return a Promise. return require('internal/crypto/ml_dsa') .mlDsaExportKey(key, kWebCryptoKeyFormatRaw); case 'ML-KEM-512': @@ -535,7 +561,6 @@ async function exportKeyRawSeed(key) { case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': - // Note: mlKemExportKey does not return a Promise. return require('internal/crypto/ml_kem') .mlKemExportKey(key, kWebCryptoKeyFormatRaw); default: @@ -543,8 +568,8 @@ async function exportKeyRawSeed(key) { } } -async function exportKeyRawSecret(key, format) { - switch (key[kAlgorithm].name) { +function exportKeyRawSecret(key, format) { + switch (getCryptoKeyAlgorithm(key).name) { case 'AES-CTR': // Fall through case 'AES-CBC': @@ -554,7 +579,7 @@ async function exportKeyRawSecret(key, format) { case 'AES-KW': // Fall through case 'HMAC': - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export()); + return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export()); case 'AES-OCB': // Fall through case 'KMAC128': @@ -563,7 +588,7 @@ async function exportKeyRawSecret(key, format) { // Fall through case 'ChaCha20-Poly1305': if (format === 'raw-secret') { - return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].export()); + return TypedArrayPrototypeGetBuffer(getCryptoKeyHandle(key).export()); } return undefined; default: @@ -571,31 +596,26 @@ async function exportKeyRawSecret(key, format) { } } -async function exportKeyJWK(key) { - const parameters = { - key_ops: key[kKeyUsages], - ext: key[kExtractable], - }; - switch (key[kAlgorithm].name) { +function exportKeyJWK(key) { + const algorithm = getCryptoKeyAlgorithm(key); + let alg; + switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': { - const alg = normalizeHashName( - key[kAlgorithm].hash.name, + alg = normalizeHashName( + algorithm.hash.name, normalizeHashName.kContextJwkRsa); - if (alg) parameters.alg = alg; break; } case 'RSA-PSS': { - const alg = normalizeHashName( - key[kAlgorithm].hash.name, + alg = normalizeHashName( + algorithm.hash.name, normalizeHashName.kContextJwkRsaPss); - if (alg) parameters.alg = alg; break; } case 'RSA-OAEP': { - const alg = normalizeHashName( - key[kAlgorithm].hash.name, + alg = normalizeHashName( + algorithm.hash.name, normalizeHashName.kContextJwkRsaOaep); - if (alg) parameters.alg = alg; break; } case 'ECDSA': @@ -611,11 +631,17 @@ async function exportKeyJWK(key) { case 'ML-DSA-65': // Fall through case 'ML-DSA-87': + // Fall through + case 'ML-KEM-512': + // Fall through + case 'ML-KEM-768': + // Fall through + case 'ML-KEM-1024': break; case 'Ed25519': // Fall through case 'Ed448': - parameters.alg = key[kAlgorithm].name; + alg = algorithm.name; break; case 'AES-CTR': // Fall through @@ -626,99 +652,96 @@ async function exportKeyJWK(key) { case 'AES-OCB': // Fall through case 'AES-KW': - parameters.alg = require('internal/crypto/aes') - .getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length); + alg = require('internal/crypto/aes') + .getAlgorithmName(algorithm.name, algorithm.length); break; case 'ChaCha20-Poly1305': - parameters.alg = 'C20P'; + alg = 'C20P'; break; case 'HMAC': { - const alg = normalizeHashName( - key[kAlgorithm].hash.name, + alg = normalizeHashName( + algorithm.hash.name, normalizeHashName.kContextJwkHmac); - if (alg) parameters.alg = alg; break; } case 'KMAC128': - parameters.alg = 'K128'; + alg = 'K128'; break; case 'KMAC256': { - parameters.alg = 'K256'; + alg = 'K256'; break; } default: return undefined; } - return key[kKeyObject][kHandle].exportJwk(parameters, true); -} - -async function exportKey(format, key) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + // Keep `alg` in the object literal so an inherited setter cannot capture + // `parameters` before native export populates key material. Delete it for + // algorithms without a JWK alg value to keep the expected shape. + const parameters = { + key_ops: ArrayPrototypeSlice(getCryptoKeyUsages(key), 0), + ext: getCryptoKeyExtractable(key), + alg, + }; + if (alg === undefined) delete parameters.alg; - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); + return getCryptoKeyHandle(key).exportJwk(parameters, true); +} +function exportKeySync(format, key) { + const algorithm = getCryptoKeyAlgorithm(key); try { - normalizeAlgorithm(key[kAlgorithm], 'exportKey'); + normalizeAlgorithm(algorithm, 'exportKey'); } catch { throw lazyDOMException( - `${key[kAlgorithm].name} key export is not supported`, 'NotSupportedError'); + `${algorithm.name} key export is not supported`, 'NotSupportedError'); } - if (!key[kExtractable]) - throw lazyDOMException('key is not extractable', 'InvalidAccessException'); + if (!getCryptoKeyExtractable(key)) + throw lazyDOMException('key is not extractable', 'InvalidAccessError'); + const type = getCryptoKeyType(key); let result; switch (format) { case 'spki': { - if (key[kKeyType] === 'public') { - result = await exportKeySpki(key); + if (type === 'public') { + result = exportKeySpki(key); } break; } case 'pkcs8': { - if (key[kKeyType] === 'private') { - result = await exportKeyPkcs8(key); + if (type === 'private') { + result = exportKeyPkcs8(key); } break; } case 'jwk': { - result = await exportKeyJWK(key); + result = exportKeyJWK(key); break; } case 'raw-secret': { - if (key[kKeyType] === 'secret') { - result = await exportKeyRawSecret(key, format); + if (type === 'secret') { + result = exportKeyRawSecret(key, format); } break; } case 'raw-public': { - if (key[kKeyType] === 'public') { - result = await exportKeyRawPublic(key, format); + if (type === 'public') { + result = exportKeyRawPublic(key, format); } break; } case 'raw-seed': { - if (key[kKeyType] === 'private') { - result = await exportKeyRawSeed(key); + if (type === 'private') { + result = exportKeyRawSeed(key); } break; } case 'raw': { - if (key[kKeyType] === 'secret') { - result = await exportKeyRawSecret(key, format); - } else if (key[kKeyType] === 'public') { - result = await exportKeyRawPublic(key, format); + if (type === 'secret') { + result = exportKeyRawSecret(key, format); + } else if (type === 'public') { + result = exportKeyRawPublic(key, format); } break; } @@ -726,13 +749,89 @@ async function exportKey(format, key) { if (!result) { throw lazyDOMException( - `Unable to export ${key[kAlgorithm].name} ${key[kKeyType]} key using ${format} format`, + `Unable to export ${algorithm.name} ${type} key using ${format} format`, 'NotSupportedError'); } return result; } +function exportKey(format, key) { + return callSubtleCryptoMethod(exportKeyImpl, this, arguments); +} + +function exportKeyImpl(format, key) { + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + + webidl ??= require('internal/crypto/webidl'); + const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + format = webidl.converters.KeyFormat(format, { + prefix, + context: '1st argument', + }); + key = webidl.converters.CryptoKey(key, { + prefix, + context: '2nd argument', + }); + + return exportKeySync(format, key); +} + +// Parsed JWK arrays are detached from Array.prototype but still need to pass +// WebIDL sequence conversion, which reads @@iterator from the value. +function safeArrayIterator() { + return new SafeArrayIterator(this); +} + +// The WebCrypto spec parses and stringifies JWKs in a fresh global object. +// Detach internal JSON values from the current global's mutable prototypes to +// approximate those fresh-realm semantics without creating a new realm. +function detachFromUserPrototypes(value) { + if (value === null || typeof value !== 'object') + return; + + ObjectSetPrototypeOf(value, null); + + if (ArrayIsArray(value)) { + setOwnProperty(value, SymbolIterator, safeArrayIterator); + for (let n = 0; n < value.length; n++) + detachFromUserPrototypes(value[n]); + return; + } + + const keys = ObjectKeys(value); + for (let n = 0; n < keys.length; n++) + detachFromUserPrototypes(value[keys[n]]); +} + +// Parse wrapped JWK bytes according to WebCrypto's "parse a JWK" procedure. +function parseJwk(keyData) { + let key; + try { + // WebCrypto parses JWKs in a fresh global. Detach parsed JSON values + // from user-mutated prototypes before WebIDL dictionary conversion. + // Wrapped JWKs may be produced outside WebCrypto, so parse using the + // spec-required UTF-8. + const json = decodeUTF8(keyData, false, true); + const result = JSONParse(json); + detachFromUserPrototypes(result); + key = webidl.converters.JsonWebKey(result); + } catch (err) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + { name: 'DataError', cause: err }); + } + + if (!ObjectPrototypeHasOwnProperty(key, 'kty')) { + throw lazyDOMException( + 'Invalid wrapped JWK key', + 'DataError'); + } + + return key; +} + function aliasKeyFormat(format) { switch (format) { case 'raw-public': @@ -846,16 +945,26 @@ function importKeySync(format, keyData, algorithm, extractable, keyUsages) { 'NotSupportedError'); } - if ((result.type === 'secret' || result.type === 'private') && result[kKeyUsages].length === 0) { + const type = getCryptoKeyType(result); + if ((type === 'secret' || type === 'private') && getCryptoKeyUsagesMask(result) === 0) { throw lazyDOMException( - `Usages cannot be empty when importing a ${result.type} key.`, + `Usages cannot be empty when importing a ${type} key.`, 'SyntaxError'); } return result; } -async function importKey( +function importKey( + format, + keyData, + algorithm, + extractable, + keyUsages) { + return callSubtleCryptoMethod(importKeyImpl, this, arguments); +} + +function importKeyImpl( format, keyData, algorithm, @@ -899,7 +1008,11 @@ async function importKey( // subtle.wrapKey() is essentially a subtle.exportKey() followed // by a subtle.encrypt(). -async function wrapKey(format, key, wrappingKey, algorithm) { +function wrapKey(format, key, wrappingKey, algorithm) { + return callSubtleCryptoMethod(wrapKeyImpl, this, arguments); +} + +function wrapKeyImpl(format, key, wrappingKey, algorithm) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -928,29 +1041,35 @@ async function wrapKey(format, key, wrappingKey, algorithm) { algorithm = normalizeAlgorithm(algorithm, 'encrypt'); } - if (algorithm.name !== wrappingKey[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(wrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(wrappingKey[kKeyUsages], 'wrapKey')) + if (!hasCryptoKeyUsage(wrappingKey, 'wrapKey')) throw lazyDOMException( 'Unable to use this key to wrapKey', 'InvalidAccessError'); - let keyData = await FunctionPrototypeCall(exportKey, this, format, key); + let keyData = exportKeySync(format, key); if (format === 'jwk') { - const ec = new TextEncoder(); - const raw = JSONStringify(keyData); + // The WebCrypto spec stringifies JWKs in a new global object. Rather + // than create a new realm here, detach this internally generated JWK from + // user-mutated prototypes so JSON.stringify cannot read inherited toJSON + // hooks from the current global. + detachFromUserPrototypes(keyData); + const json = JSONStringify(keyData); // As per the NOTE in step 13 https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey // we're padding AES-KW wrapped JWK to make sure it is always a multiple of 8 bytes // in length - if (algorithm.name === 'AES-KW' && raw.length % 8 !== 0) { - keyData = ec.encode(raw + StringPrototypeRepeat(' ', 8 - (raw.length % 8))); + // The spec then UTF-8 encodes json. + if (algorithm.name === 'AES-KW' && json.length % 8 !== 0) { + keyData = encodeUtf8String( + json + StringPrototypeRepeat(' ', 8 - (json.length % 8))); } else { - keyData = ec.encode(raw); + keyData = encodeUtf8String(json); } } - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, algorithm, wrappingKey, @@ -960,7 +1079,18 @@ async function wrapKey(format, key, wrappingKey, algorithm) { // subtle.unwrapKey() is essentially a subtle.decrypt() followed // by a subtle.importKey(). -async function unwrapKey( +function unwrapKey( + format, + wrappedKey, + unwrappingKey, + unwrapAlgo, + unwrappedKeyAlgo, + extractable, + keyUsages) { + return callSubtleCryptoMethod(unwrapKeyImpl, this, arguments); +} + +function unwrapKeyImpl( format, wrappedKey, unwrappingKey, @@ -1013,88 +1143,83 @@ async function unwrapKey( unwrappedKeyAlgo = normalizeAlgorithm(unwrappedKeyAlgo, 'importKey'); - if (unwrapAlgo.name !== unwrappingKey[kAlgorithm].name) + if (unwrapAlgo.name !== getCryptoKeyAlgorithm(unwrappingKey).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(unwrappingKey[kKeyUsages], 'unwrapKey')) + if (!hasCryptoKeyUsage(unwrappingKey, 'unwrapKey')) throw lazyDOMException( 'Unable to use this key to unwrapKey', 'InvalidAccessError'); - let keyData = await cipherOrWrap( + const keyData = cipherOrWrap( kWebCryptoCipherDecrypt, unwrapAlgo, unwrappingKey, wrappedKey, 'unwrapKey'); - if (format === 'jwk') { - // The fatal: true option is only supported in builds that have ICU. - const options = process.versions.icu !== undefined ? - { fatal: true } : undefined; - const dec = new TextDecoder('utf-8', options); - try { - keyData = JSONParse(dec.decode(keyData)); - } catch { - throw lazyDOMException('Invalid wrapped JWK key', 'DataError'); + return jobPromiseThen(keyData, (keyData) => { + if (format === 'jwk') { + keyData = parseJwk(keyData); } - } - return FunctionPrototypeCall( - importKeySync, - this, - format, keyData, unwrappedKeyAlgo, extractable, keyUsages, - ); + return FunctionPrototypeCall( + importKeySync, + this, + format, keyData, unwrappedKeyAlgo, extractable, keyUsages, + ); + }); } -async function signVerify(algorithm, key, data, signature) { - let usage = 'sign'; - if (signature !== undefined) { - usage = 'verify'; - } - algorithm = normalizeAlgorithm(algorithm, usage); +function signVerify(algorithm, key, data, signature) { + const op = signature !== undefined ? 'verify' : 'sign'; // This is also usage + algorithm = normalizeAlgorithm(algorithm, op); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], usage)) + if (!hasCryptoKeyUsage(key, op)) throw lazyDOMException( - `Unable to use this key to ${usage}`, 'InvalidAccessError'); + `Unable to use this key to ${op}`, 'InvalidAccessError'); switch (algorithm.name) { case 'RSA-PSS': // Fall through case 'RSASSA-PKCS1-v1_5': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaSignVerify(key, data, algorithm, signature); case 'ECDSA': - return await require('internal/crypto/ec') + return require('internal/crypto/ec') .ecdsaSignVerify(key, data, algorithm, signature); case 'Ed25519': // Fall through case 'Ed448': // Fall through - return await require('internal/crypto/cfrg') + return require('internal/crypto/cfrg') .eddsaSignVerify(key, data, algorithm, signature); case 'HMAC': - return await require('internal/crypto/mac') + return require('internal/crypto/mac') .hmacSignVerify(key, data, algorithm, signature); case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': - return await require('internal/crypto/ml_dsa') + return require('internal/crypto/ml_dsa') .mlDsaSignVerify(key, data, algorithm, signature); case 'KMAC128': // Fall through case 'KMAC256': - return await require('internal/crypto/mac') + return require('internal/crypto/mac') .kmacSignVerify(key, data, algorithm, signature); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function sign(algorithm, key, data) { +function sign(algorithm, key, data) { + return callSubtleCryptoMethod(signImpl, this, arguments); +} + +function signImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1113,10 +1238,14 @@ async function sign(algorithm, key, data) { context: '3rd argument', }); - return await signVerify(algorithm, key, data); + return signVerify(algorithm, key, data); +} + +function verify(algorithm, key, signature, data) { + return callSubtleCryptoMethod(verifyImpl, this, arguments); } -async function verify(algorithm, key, signature, data) { +function verifyImpl(algorithm, key, signature, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1139,10 +1268,10 @@ async function verify(algorithm, key, signature, data) { context: '4th argument', }); - return await signVerify(algorithm, key, data, signature); + return signVerify(algorithm, key, data, signature); } -async function cipherOrWrap(mode, algorithm, key, data, op) { +function cipherOrWrap(mode, algorithm, key, data, op) { // While WebCrypto allows for larger input buffer sizes, we limit // those to sizes that can fit within uint32_t because of limitations // in the OpenSSL API. @@ -1150,7 +1279,7 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { switch (algorithm.name) { case 'RSA-OAEP': - return await require('internal/crypto/rsa') + return require('internal/crypto/rsa') .rsaCipher(mode, key, data, algorithm); case 'AES-CTR': // Fall through @@ -1159,21 +1288,25 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-GCM': // Fall through case 'AES-OCB': - return await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); case 'ChaCha20-Poly1305': - return await require('internal/crypto/chacha20_poly1305') + return require('internal/crypto/chacha20_poly1305') .c20pCipher(mode, key, data, algorithm); case 'AES-KW': if (op === 'wrapKey' || op === 'unwrapKey') { - return await require('internal/crypto/aes') + return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); } } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encrypt(algorithm, key, data) { +function encrypt(algorithm, key, data) { + return callSubtleCryptoMethod(encryptImpl, this, arguments); +} + +function encryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1194,14 +1327,14 @@ async function encrypt(algorithm, key, data) { algorithm = normalizeAlgorithm(algorithm, 'encrypt'); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], 'encrypt')) + if (!hasCryptoKeyUsage(key, 'encrypt')) throw lazyDOMException( 'Unable to use this key to encrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherEncrypt, algorithm, key, @@ -1210,7 +1343,11 @@ async function encrypt(algorithm, key, data) { ); } -async function decrypt(algorithm, key, data) { +function decrypt(algorithm, key, data) { + return callSubtleCryptoMethod(decryptImpl, this, arguments); +} + +function decryptImpl(algorithm, key, data) { if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); webidl ??= require('internal/crypto/webidl'); @@ -1231,14 +1368,14 @@ async function decrypt(algorithm, key, data) { algorithm = normalizeAlgorithm(algorithm, 'decrypt'); - if (algorithm.name !== key[kAlgorithm].name) + if (algorithm.name !== getCryptoKeyAlgorithm(key).name) throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError'); - if (!ArrayPrototypeIncludes(key[kKeyUsages], 'decrypt')) + if (!hasCryptoKeyUsage(key, 'decrypt')) throw lazyDOMException( 'Unable to use this key to decrypt', 'InvalidAccessError'); - return await cipherOrWrap( + return cipherOrWrap( kWebCryptoCipherDecrypt, algorithm, key, @@ -1248,7 +1385,11 @@ async function decrypt(algorithm, key, data) { } // Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-getPublicKey -async function getPublicKey(key, keyUsages) { +function getPublicKey(key, keyUsages) { + return callSubtleCryptoMethod(getPublicKeyImpl, this, arguments); +} + +function getPublicKeyImpl(key, keyUsages) { emitExperimentalWarning('The getPublicKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1264,16 +1405,22 @@ async function getPublicKey(key, keyUsages) { context: '2nd argument', }); - if (key[kKeyType] !== 'private') + const type = getCryptoKeyType(key); + if (type !== 'private') throw lazyDOMException('key must be a private key', - key[kKeyType] === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); + type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - const keyObject = createPublicKey(key[kKeyObject]); + // TODO(panva): this is by no means a hot path, but let's still follow up to get + // rid of this awkwardness + const keyObject = createPublicKey(new PrivateKeyObject(getCryptoKeyHandle(key))); + return keyObject.toCryptoKey(getCryptoKeyAlgorithm(key), true, keyUsages); +} - return keyObject.toCryptoKey(key[kAlgorithm], true, keyUsages); +function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { + return callSubtleCryptoMethod(encapsulateBitsImpl, this, arguments); } -async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { +function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1289,32 +1436,48 @@ async function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { context: '2nd argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); - if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) { + if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateBits')) { + if (!hasCryptoKeyUsage(encapsulationKey, 'encapsulateBits')) { throw lazyDOMException( 'encapsulationKey does not have encapsulateBits usage', 'InvalidAccessError'); } - switch (encapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages) { +function encapsulateKey( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + usages) { + return callSubtleCryptoMethod(encapsulateKeyImpl, this, arguments); +} + +function encapsulateKeyImpl( + encapsulationAlgorithm, + encapsulationKey, + sharedKeyAlgorithm, + extractable, + usages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1342,48 +1505,56 @@ async function encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKe context: '5th argument', }); - const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedEncapsulationAlgorithm = + normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const keyAlgorithm = getCryptoKeyAlgorithm(encapsulationKey); - if (normalizedEncapsulationAlgorithm.name !== encapsulationKey[kAlgorithm].name) { + if (normalizedEncapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(encapsulationKey[kKeyUsages], 'encapsulateKey')) { + if (!hasCryptoKeyUsage(encapsulationKey, 'encapsulateKey')) { throw lazyDOMException( 'encapsulationKey does not have encapsulateKey usage', 'InvalidAccessError'); } let encapsulateBits; - switch (encapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - encapsulateBits = await require('internal/crypto/ml_kem') + encapsulateBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - const sharedKey = FunctionPrototypeCall( - importKeySync, - this, - 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, extractable, usages, - ); - - const encapsulatedKey = { - ciphertext: encapsulateBits.ciphertext, - sharedKey, - }; + return jobPromiseThen(encapsulateBits, (encapsulateBits) => { + const sharedKey = FunctionPrototypeCall( + importKeySync, + this, + 'raw-secret', encapsulateBits.sharedKey, normalizedSharedKeyAlgorithm, + extractable, usages, + ); + + return { + ciphertext: encapsulateBits.ciphertext, + sharedKey, + }; + }); +} - return encapsulatedKey; +function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { + return callSubtleCryptoMethod(decapsulateBitsImpl, this, arguments); } -async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext) { +function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertext) { emitExperimentalWarning('The decapsulateBits Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1403,34 +1574,46 @@ async function decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphert context: '3rd argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); - if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) { + if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateBits')) { + if (!hasCryptoKeyUsage(decapsulationKey, 'decapsulateBits')) { throw lazyDOMException( 'decapsulationKey does not have decapsulateBits usage', 'InvalidAccessError'); } - switch (decapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - return await require('internal/crypto/ml_kem') + return require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); } throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } -async function decapsulateKey( +function decapsulateKey( decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages, ) { + return callSubtleCryptoMethod(decapsulateKeyImpl, this, arguments); +} + +function decapsulateKeyImpl( + decapsulationAlgorithm, + decapsulationKey, + ciphertext, + sharedKeyAlgorithm, + extractable, + usages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); @@ -1462,38 +1645,42 @@ async function decapsulateKey( context: '6th argument', }); - const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); - const normalizedSharedKeyAlgorithm = normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const normalizedDecapsulationAlgorithm = + normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); + const normalizedSharedKeyAlgorithm = + normalizeAlgorithm(sharedKeyAlgorithm, 'importKey'); + const keyAlgorithm = getCryptoKeyAlgorithm(decapsulationKey); - if (normalizedDecapsulationAlgorithm.name !== decapsulationKey[kAlgorithm].name) { + if (normalizedDecapsulationAlgorithm.name !== keyAlgorithm.name) { throw lazyDOMException( 'key algorithm mismatch', 'InvalidAccessError'); } - if (!ArrayPrototypeIncludes(decapsulationKey[kKeyUsages], 'decapsulateKey')) { + if (!hasCryptoKeyUsage(decapsulationKey, 'decapsulateKey')) { throw lazyDOMException( 'decapsulationKey does not have decapsulateKey usage', 'InvalidAccessError'); } let decapsulatedBits; - switch (decapsulationKey[kAlgorithm].name) { + switch (keyAlgorithm.name) { case 'ML-KEM-512': case 'ML-KEM-768': case 'ML-KEM-1024': - decapsulatedBits = await require('internal/crypto/ml_kem') + decapsulatedBits = require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } - return FunctionPrototypeCall( + return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( importKeySync, this, - 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, usages, - ); + 'raw-secret', decapsulatedBits, normalizedSharedKeyAlgorithm, extractable, + usages, + )); } // The SubtleCrypto and Crypto classes are defined as part of the diff --git a/lib/internal/crypto/webcrypto_util.js b/lib/internal/crypto/webcrypto_util.js new file mode 100644 index 00000000000000..320bf0436de553 --- /dev/null +++ b/lib/internal/crypto/webcrypto_util.js @@ -0,0 +1,145 @@ +'use strict'; + +const { + KeyObjectHandle, + kKeyFormatDER, + kKeyFormatJWK, + kKeyEncodingPKCS8, + kKeyEncodingSPKI, + kKeyTypePublic, + kKeyTypePrivate, + kKeyTypeSecret, +} = internalBinding('crypto'); + +const { + validateKeyOps, +} = require('internal/crypto/util'); + +const { + lazyDOMException, +} = require('internal/util'); + +function importDerKey(keyData, isPublic) { + const handle = new KeyObjectHandle(); + const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + const encoding = isPublic ? kKeyEncodingSPKI : kKeyEncodingPKCS8; + try { + handle.init(keyType, keyData, kKeyFormatDER, encoding, null, null); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + return handle; +} + +function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) { + if (typeof keyData.kty !== 'string') + throw lazyDOMException('Invalid keyData', 'DataError'); + if (keyData.kty !== kty) + throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + switch (kty) { + case 'RSA': + if (typeof keyData.n !== 'string' || + typeof keyData.e !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + if (typeof keyData.d === 'string' && + (typeof keyData.p !== 'string' || + typeof keyData.q !== 'string' || + typeof keyData.dp !== 'string' || + typeof keyData.dq !== 'string' || + typeof keyData.qi !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'EC': + if (typeof keyData.crv !== 'string' || + typeof keyData.x !== 'string' || + typeof keyData.y !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'OKP': + if (typeof keyData.crv !== 'string' || + typeof keyData.x !== 'string' || + (keyData.d !== undefined && typeof keyData.d !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'oct': + if (typeof keyData.k !== 'string') + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + case 'AKP': + if (typeof keyData.alg !== 'string' || + typeof keyData.pub !== 'string' || + (keyData.priv !== undefined && typeof keyData.priv !== 'string')) + throw lazyDOMException('Invalid keyData', 'DataError'); + break; + default: { + // It is not possible to get here because all possible cases are handled above. + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + } + if (usagesSet.size > 0 && keyData.use !== undefined) { + if (keyData.use !== expectedUse) + throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); + } + validateKeyOps(keyData.key_ops, usagesSet); + if (keyData.ext !== undefined && + keyData.ext === false && + extractable === true) { + throw lazyDOMException( + 'JWK "ext" Parameter and extractable mismatch', + 'DataError'); + } +} + +function importJwkKey(isPublic, keyData) { + const handle = new KeyObjectHandle(); + const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + try { + handle.init(keyType, keyData, kKeyFormatJWK, null, null, null); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + return handle; +} + +function importRawKey(isPublic, keyData, format, name, namedCurve) { + const handle = new KeyObjectHandle(); + const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; + try { + handle.init(keyType, keyData, format, name ?? null, null, namedCurve ?? null); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + return handle; +} + +function importSecretKey(keyData) { + const handle = new KeyObjectHandle(); + handle.init(kKeyTypeSecret, keyData); + return handle; +} + +function importJwkSecretKey(keyData) { + const handle = new KeyObjectHandle(); + try { + handle.init(kKeyTypeSecret, keyData, kKeyFormatJWK, null, null); + } catch (err) { + throw lazyDOMException( + 'Invalid keyData', { name: 'DataError', cause: err }); + } + return handle; +} + +module.exports = { + importDerKey, + importJwkKey, + importJwkSecretKey, + importRawKey, + importSecretKey, + validateJwk, +}; diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index 1271373c6d1009..f4ae6a8191720a 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -1,101 +1,36 @@ 'use strict'; -// Adapted from the following sources -// - https://github.com/jsdom/webidl-conversions -// Copyright Domenic Denicola. Licensed under BSD-2-Clause License. -// Original license at https://github.com/jsdom/webidl-conversions/blob/master/LICENSE.md. -// - https://github.com/denoland/deno -// Copyright Deno authors. Licensed under MIT License. -// Original license at https://github.com/denoland/deno/blob/main/LICENSE.md. -// Changes include using primordials and stripping the code down to only what -// WebCryptoAPI needs. - const { - ArrayBufferIsView, ArrayPrototypeIncludes, - ArrayPrototypePush, - ArrayPrototypeSort, MathPow, - MathTrunc, - Number, - NumberIsFinite, NumberParseInt, ObjectPrototypeHasOwnProperty, - ObjectPrototypeIsPrototypeOf, - SafeArrayIterator, - String, StringPrototypeStartsWith, StringPrototypeToLowerCase, - TypedArrayPrototypeGetBuffer, - TypedArrayPrototypeGetSymbolToStringTag, } = primordials; -const { - converters: sharedConverters, - makeException, - createEnumConverter, - createSequenceConverter, -} = require('internal/webidl'); - const { lazyDOMException, kEmptyObject, - setOwnProperty, } = require('internal/util'); const { CryptoKey } = require('internal/crypto/webcrypto'); +const { + getCryptoKeyAlgorithm, + getCryptoKeyType, +} = require('internal/crypto/keys'); const { validateMaxBufferLength, kNamedCurveAliases, } = require('internal/crypto/util'); -const { isSharedArrayBuffer } = require('internal/util/types'); - -// https://tc39.es/ecma262/#sec-tonumber -function toNumber(value, opts = kEmptyObject) { - switch (typeof value) { - case 'number': - return value; - case 'bigint': - throw makeException( - 'is a BigInt and cannot be converted to a number.', - opts); - case 'symbol': - throw makeException( - 'is a Symbol and cannot be converted to a number.', - opts); - default: - return Number(value); - } -} - -function type(V) { - if (V === null) - return 'Null'; - - switch (typeof V) { - case 'undefined': - return 'Undefined'; - case 'boolean': - return 'Boolean'; - case 'number': - return 'Number'; - case 'string': - return 'String'; - case 'symbol': - return 'Symbol'; - case 'bigint': - return 'BigInt'; - case 'object': // Fall through - case 'function': // Fall through - default: - // Per ES spec, typeof returns an implementation-defined value that is not - // any of the existing ones for uncallable non-standard exotic objects. - // Yet Type() which the Web IDL spec depends on returns Object for such - // cases. So treat the default case as an object. - return 'Object'; - } -} - -const integerPart = MathTrunc; +const { + converters: webidl, + createDictionaryConverter, + createEnumConverter, + createInterfaceConverter, + createSequenceConverter, + requiredArguments, + type, +} = require('internal/webidl'); function validateByteLength(buf, name, target) { if (buf.byteLength !== target) { @@ -119,79 +54,7 @@ function namedCurveValidator(V, dict) { 'NotSupportedError'); } -// This was updated to only consider bitlength up to 32 used by WebCryptoAPI -function createIntegerConversion(bitLength) { - const lowerBound = 0; - const upperBound = MathPow(2, bitLength) - 1; - - const twoToTheBitLength = MathPow(2, bitLength); - - return (V, opts = kEmptyObject) => { - let x = toNumber(V, opts); - - if (opts.enforceRange) { - if (!NumberIsFinite(x)) { - throw makeException( - 'is not a finite number.', - opts); - } - - x = integerPart(x); - - if (x < lowerBound || x > upperBound) { - throw makeException( - `is outside the expected range of ${lowerBound} to ${upperBound}.`, - { __proto__: null, ...opts, code: 'ERR_OUT_OF_RANGE' }, - ); - } - - return x; - } - - if (!NumberIsFinite(x) || x === 0) { - return 0; - } - - x = integerPart(x); - - if (x >= lowerBound && x <= upperBound) { - return x; - } - - x = x % twoToTheBitLength; - - return x; - }; -} - -const converters = {}; - -converters.boolean = (val) => !!val; -converters.octet = createIntegerConversion(8); -converters['unsigned short'] = createIntegerConversion(16); -converters['unsigned long'] = createIntegerConversion(32); - -converters.DOMString = function(V, opts = kEmptyObject) { - if (typeof V === 'string') { - return V; - } else if (typeof V === 'symbol') { - throw makeException( - 'is a Symbol and cannot be converted to a string.', - opts); - } - - return String(V); -}; - -converters.object = (V, opts) => { - if (type(V) !== 'Object') { - throw makeException( - 'is not an object.', - opts); - } - - return V; -}; +const converters = { __proto__: null, ...webidl }; /** * @param {string | object} V - The hash algorithm identifier (string or object). @@ -205,117 +68,6 @@ function ensureSHA(V, label) { `Only SHA hashes are supported in ${label}`, 'NotSupportedError'); } -converters.Uint8Array = (V, opts = kEmptyObject) => { - if (!ArrayBufferIsView(V) || - TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') { - throw makeException( - 'is not an Uint8Array object.', - opts); - } - if (isSharedArrayBuffer(TypedArrayPrototypeGetBuffer(V))) { - throw makeException( - 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); - } - - return V; -}; - -converters.BufferSource = sharedConverters.BufferSource; - -converters['sequence'] = createSequenceConverter( - converters.DOMString); - -function requiredArguments(length, required, opts = kEmptyObject) { - if (length < required) { - throw makeException( - `${required} argument${ - required === 1 ? '' : 's' - } required, but only ${length} present.`, - { __proto__: null, ...opts, context: '', code: 'ERR_MISSING_ARGS' }); - } -} - -function createDictionaryConverter(name, dictionaries) { - let hasRequiredKey = false; - const allMembers = []; - for (let i = 0; i < dictionaries.length; i++) { - const member = dictionaries[i]; - if (member.required) { - hasRequiredKey = true; - } - ArrayPrototypePush(allMembers, member); - } - ArrayPrototypeSort(allMembers, (a, b) => { - if (a.key === b.key) { - return 0; - } - return a.key < b.key ? -1 : 1; - }); - - return function(V, opts = kEmptyObject) { - const typeV = type(V); - switch (typeV) { - case 'Undefined': - case 'Null': - case 'Object': - break; - default: - throw makeException( - 'can not be converted to a dictionary', - opts); - } - const esDict = V; - const idlDict = {}; - - // Fast path null and undefined. - if (V == null && !hasRequiredKey) { - return idlDict; - } - - for (const member of new SafeArrayIterator(allMembers)) { - const key = member.key; - - let esMemberValue; - if (typeV === 'Undefined' || typeV === 'Null') { - esMemberValue = undefined; - } else { - esMemberValue = esDict[key]; - } - - if (esMemberValue !== undefined) { - const context = `'${key}' of '${name}'${ - opts.context ? ` (${opts.context})` : '' - }`; - const idlMemberValue = member.converter(esMemberValue, { - __proto__: null, - ...opts, - context, - }); - member.validator?.(idlMemberValue, esDict); - setOwnProperty(idlDict, key, idlMemberValue); - } else if (member.required) { - throw makeException( - `can not be converted to '${name}' because '${key}' is required in '${name}'.`, - { __proto__: null, ...opts, code: 'ERR_MISSING_OPTION' }); - } - } - - return idlDict; - }; -} - -function createInterfaceConverter(name, prototype) { - return (V, opts) => { - if (!ObjectPrototypeIsPrototypeOf(prototype, V)) { - throw makeException( - `is not of type ${name}.`, - opts); - } - return V; - }; -} - converters.AlgorithmIdentifier = (V, opts) => { // Union for (object or DOMString) if (type(V) === 'Object') { @@ -365,10 +117,29 @@ const dictAlgorithm = [ converters.Algorithm = createDictionaryConverter( 'Algorithm', dictAlgorithm); -converters.BigInteger = converters.Uint8Array; +// TODO(panva): Reject resizable backing stores in a semver-major with: +// converters.BigInteger = webidl.Uint8Array; +converters.BigInteger = (V, opts = kEmptyObject) => { + return webidl.Uint8Array(V, { + __proto__: null, + ...opts, + allowResizable: true, + allowShared: false, + }); +}; + +// TODO(panva): Reject resizable backing stores in a semver-major by +// removing this altogether. +converters.BufferSource = (V, opts = kEmptyObject) => { + return webidl.BufferSource(V, { + __proto__: null, + ...opts, + allowResizable: opts.allowResizable === undefined ? + true : opts.allowResizable, + }); +}; const dictRsaKeyGenParams = [ - ...new SafeArrayIterator(dictAlgorithm), { key: 'modulusLength', converter: (V, opts) => @@ -383,64 +154,78 @@ const dictRsaKeyGenParams = [ ]; converters.RsaKeyGenParams = createDictionaryConverter( - 'RsaKeyGenParams', dictRsaKeyGenParams); + 'RsaKeyGenParams', [ + dictAlgorithm, + dictRsaKeyGenParams, + ]); converters.RsaHashedKeyGenParams = createDictionaryConverter( 'RsaHashedKeyGenParams', [ - ...new SafeArrayIterator(dictRsaKeyGenParams), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), - required: true, - }, + dictAlgorithm, + dictRsaKeyGenParams, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'RsaHashedKeyGenParams'), + required: true, + }, + ], ]); converters.RsaHashedImportParams = createDictionaryConverter( 'RsaHashedImportParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), - required: true, - }, + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'RsaHashedImportParams'), + required: true, + }, + ], ]); converters.NamedCurve = converters.DOMString; converters.EcKeyImportParams = createDictionaryConverter( 'EcKeyImportParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'namedCurve', - converter: converters.NamedCurve, - validator: namedCurveValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'namedCurve', + converter: converters.NamedCurve, + validator: namedCurveValidator, + required: true, + }, + ], ]); converters.EcKeyGenParams = createDictionaryConverter( 'EcKeyGenParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'namedCurve', - converter: converters.NamedCurve, - validator: namedCurveValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'namedCurve', + converter: converters.NamedCurve, + validator: namedCurveValidator, + required: true, + }, + ], ]); converters.AesKeyGenParams = createDictionaryConverter( 'AesKeyGenParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), - validator: AESLengthValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned short'](V, { ...opts, enforceRange: true }), + validator: AESLengthValidator, + required: true, + }, + ], ]); function validateZeroLength(parameterName) { @@ -454,51 +239,59 @@ function validateZeroLength(parameterName) { converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'saltLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - required: true, - }, + dictAlgorithm, + [ + { + key: 'saltLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + required: true, + }, + ], ]); converters.RsaOaepParams = createDictionaryConverter( 'RsaOaepParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'label', - converter: converters.BufferSource, - }, + dictAlgorithm, + [ + { + key: 'label', + converter: converters.BufferSource, + }, + ], ]); converters.EcdsaParams = createDictionaryConverter( 'EcdsaParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), - required: true, - }, - ]); - -for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { - converters[name] = createDictionaryConverter( - name, [ - ...new SafeArrayIterator(dictAlgorithm), + dictAlgorithm, + [ { key: 'hash', converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, name), + validator: (V, dict) => ensureSHA(V, 'EcdsaParams'), required: true, }, - { - key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: validateMacKeyLength(`${name}.length`, zeroError), - }, + ], + ]); + +for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { + converters[name] = createDictionaryConverter( + name, [ + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, name), + required: true, + }, + { + key: 'length', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: validateMacKeyLength(`${name}.length`, zeroError), + }, + ], ]); } @@ -537,195 +330,210 @@ converters.JsonWebKey = createDictionaryConverter( simpleDomStringKey('dp'), simpleDomStringKey('dq'), simpleDomStringKey('qi'), - simpleDomStringKey('pub'), - simpleDomStringKey('priv'), { key: 'oth', converter: converters['sequence'], }, simpleDomStringKey('k'), + simpleDomStringKey('pub'), + simpleDomStringKey('priv'), ]); converters.HkdfParams = createDictionaryConverter( 'HkdfParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'HkdfParams'), - required: true, - }, - { - key: 'salt', - converter: converters.BufferSource, - required: true, - }, - { - key: 'info', - converter: converters.BufferSource, - required: true, - }, + dictAlgorithm, + [ + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'HkdfParams'), + required: true, + }, + { + key: 'salt', + converter: converters.BufferSource, + required: true, + }, + { + key: 'info', + converter: converters.BufferSource, + validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.info', 1024), + required: true, + }, + ], ]); converters.CShakeParams = createDictionaryConverter( 'CShakeParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - // The Web Crypto spec allows for SHAKE output length that are not multiples of - // 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); - }, - required: true, - }, - { - key: 'functionName', - converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.functionName'), - }, - { - key: 'customization', - converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.customization'), - }, + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + // The Web Crypto spec allows for SHAKE output length that are not multiples of + // 8. We don't. + if (V % 8) + throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); + }, + required: true, + }, + { + key: 'functionName', + converter: converters.BufferSource, + validator: validateZeroLength('CShakeParams.functionName'), + }, + { + key: 'customization', + converter: converters.BufferSource, + validator: validateZeroLength('CShakeParams.customization'), + }, + ], ]); converters.Pbkdf2Params = createDictionaryConverter( 'Pbkdf2Params', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'hash', - converter: converters.HashAlgorithmIdentifier, - validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), - required: true, - }, - { - key: 'iterations', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0) - throw lazyDOMException('iterations cannot be zero', 'OperationError'); - }, - required: true, - }, - { - key: 'salt', - converter: converters.BufferSource, - required: true, - }, + dictAlgorithm, + [ + { + key: 'salt', + converter: converters.BufferSource, + required: true, + }, + { + key: 'iterations', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0) + throw lazyDOMException('iterations cannot be zero', 'OperationError'); + }, + required: true, + }, + { + key: 'hash', + converter: converters.HashAlgorithmIdentifier, + validator: (V, dict) => ensureSHA(V, 'Pbkdf2Params'), + required: true, + }, + ], ]); converters.AesDerivedKeyParams = createDictionaryConverter( 'AesDerivedKeyParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned short'](V, { ...opts, enforceRange: true }), - validator: AESLengthValidator, - required: true, - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned short'](V, { ...opts, enforceRange: true }), + validator: AESLengthValidator, + required: true, + }, + ], ]); converters.AesCbcParams = createDictionaryConverter( 'AesCbcParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'iv', - converter: converters.BufferSource, - validator: (V, dict) => validateByteLength(V, 'algorithm.iv', 16), - required: true, - }, + dictAlgorithm, + [ + { + key: 'iv', + converter: converters.BufferSource, + validator: (V, dict) => validateByteLength(V, 'algorithm.iv', 16), + required: true, + }, + ], ]); converters.AeadParams = createDictionaryConverter( 'AeadParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'iv', - converter: converters.BufferSource, - validator: (V, dict) => { - switch (StringPrototypeToLowerCase(dict.name)) { - case 'chacha20-poly1305': - validateByteLength(V, 'algorithm.iv', 12); - break; - case 'aes-gcm': - validateMaxBufferLength(V, 'algorithm.iv'); - break; - case 'aes-ocb': - if (V.byteLength > 15) { - throw lazyDOMException( - 'AES-OCB algorithm.iv must be no more than 15 bytes', - 'OperationError'); - } - break; - } + dictAlgorithm, + [ + { + key: 'iv', + converter: converters.BufferSource, + validator: (V, dict) => { + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + validateByteLength(V, 'algorithm.iv', 12); + break; + case 'aes-gcm': + validateMaxBufferLength(V, 'algorithm.iv'); + break; + case 'aes-ocb': + if (V.byteLength > 15) { + throw lazyDOMException( + 'AES-OCB algorithm.iv must be no more than 15 bytes', + 'OperationError'); + } + break; + } + }, + required: true, }, - required: true, - }, - { - key: 'tagLength', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - switch (StringPrototypeToLowerCase(dict.name)) { - case 'chacha20-poly1305': - if (V !== 128) { - throw lazyDOMException( - `${V} is not a valid ChaCha20-Poly1305 tag length`, - 'OperationError'); - } - break; - case 'aes-gcm': - if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { - throw lazyDOMException( - `${V} is not a valid AES-GCM tag length`, - 'OperationError'); - } - break; - case 'aes-ocb': - if (!ArrayPrototypeIncludes([64, 96, 128], V)) { - throw lazyDOMException( - `${V} is not a valid AES-OCB tag length`, - 'OperationError'); - } - break; - } + { + key: 'additionalData', + converter: converters.BufferSource, + validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.additionalData'), }, - }, - { - key: 'additionalData', - converter: converters.BufferSource, - validator: (V, dict) => validateMaxBufferLength(V, 'algorithm.additionalData'), - }, + { + key: 'tagLength', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + switch (StringPrototypeToLowerCase(dict.name)) { + case 'chacha20-poly1305': + if (V !== 128) { + throw lazyDOMException( + `${V} is not a valid ChaCha20-Poly1305 tag length`, + 'OperationError'); + } + break; + case 'aes-gcm': + if (!ArrayPrototypeIncludes([32, 64, 96, 104, 112, 120, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-GCM tag length`, + 'OperationError'); + } + break; + case 'aes-ocb': + if (!ArrayPrototypeIncludes([64, 96, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-OCB tag length`, + 'OperationError'); + } + break; + } + }, + }, + ], ]); converters.AesCtrParams = createDictionaryConverter( 'AesCtrParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'counter', - converter: converters.BufferSource, - validator: (V, dict) => validateByteLength(V, 'algorithm.counter', 16), - required: true, - }, - { - key: 'length', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0 || V > 128) - throw lazyDOMException( - 'AES-CTR algorithm.length must be between 1 and 128', - 'OperationError'); - }, - required: true, - }, + dictAlgorithm, + [ + { + key: 'counter', + converter: converters.BufferSource, + validator: (V, dict) => validateByteLength(V, 'algorithm.counter', 16), + required: true, + }, + { + key: 'length', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0 || V > 128) + throw lazyDOMException( + 'AES-CTR algorithm.length must be between 1 and 128', + 'OperationError'); + }, + required: true, + }, + ], ]); converters.CryptoKey = createInterfaceConverter( @@ -733,104 +541,124 @@ converters.CryptoKey = createInterfaceConverter( converters.EcdhKeyDeriveParams = createDictionaryConverter( 'EcdhKeyDeriveParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'public', - converter: converters.CryptoKey, - validator: (V, dict) => { - if (V.type !== 'public') - throw lazyDOMException( - 'algorithm.public must be a public key', 'InvalidAccessError'); - - if (StringPrototypeToLowerCase(V.algorithm.name) !== StringPrototypeToLowerCase(dict.name)) - throw lazyDOMException( - 'key algorithm mismatch', - 'InvalidAccessError'); - }, - required: true, - }, + dictAlgorithm, + [ + { + key: 'public', + converter: converters.CryptoKey, + validator: (V, dict) => { + if (getCryptoKeyType(V) !== 'public') + throw lazyDOMException( + 'algorithm.public must be a public key', 'InvalidAccessError'); + + if (StringPrototypeToLowerCase(getCryptoKeyAlgorithm(V).name) !== StringPrototypeToLowerCase(dict.name)) + throw lazyDOMException( + 'key algorithm mismatch', + 'InvalidAccessError'); + }, + required: true, + }, + ], ]); converters.ContextParams = createDictionaryConverter( 'ContextParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'context', - converter: converters.BufferSource, - validator(V, dict) { - let { 0: major, 1: minor } = process.versions.openssl.split('.'); - major = NumberParseInt(major, 10); - minor = NumberParseInt(minor, 10); - if (major > 3 || (major === 3 && minor >= 2)) { - this.validator = undefined; - } else { - this.validator = validateZeroLength('ContextParams.context'); - this.validator(V, dict); - } + dictAlgorithm, + [ + { + key: 'context', + converter: converters.BufferSource, + validator(V, dict) { + if (process.features.openssl_is_boringssl) { + this.validator = undefined; + } else { + let { 0: major, 1: minor } = process.versions.openssl.split('.'); + major = NumberParseInt(major, 10); + minor = NumberParseInt(minor, 10); + if (major > 3 || (major === 3 && minor >= 2)) { + this.validator = undefined; + } else { + this.validator = validateZeroLength('ContextParams.context'); + this.validator(V, dict); + } + } + }, }, - }, + ], ]); converters.Argon2Params = createDictionaryConverter( 'Argon2Params', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'nonce', - converter: converters.BufferSource, - required: true, - }, - { - key: 'parallelism', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V === 0 || V > MathPow(2, 24) - 1) { - throw lazyDOMException( - 'parallelism must be > 0 and < 16777215', - 'OperationError'); - } - }, - required: true, - }, - { - key: 'memory', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V < 8 * dict.parallelism) { - throw lazyDOMException( - 'memory must be at least 8 times the degree of parallelism', - 'OperationError'); - } - }, - required: true, - }, - { - key: 'passes', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - required: true, - }, - { - key: 'version', - converter: (V, opts) => - converters.octet(V, { ...opts, enforceRange: true }), - validator: (V, dict) => { - if (V !== 0x13) { - throw lazyDOMException( - `${V} is not a valid Argon2 version`, - 'OperationError'); - } + dictAlgorithm, + [ + { + key: 'nonce', + converter: converters.BufferSource, + validator: (V) => { + if (V.byteLength < 8) { + throw lazyDOMException('nonce must be at least 8 bytes', 'OperationError'); + } + }, + required: true, }, - }, - { - key: 'secretValue', - converter: converters.BufferSource, - }, - { - key: 'associatedData', - converter: converters.BufferSource, - }, + { + key: 'parallelism', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V === 0 || V > MathPow(2, 24) - 1) { + throw lazyDOMException( + 'parallelism must be > 0 and <= 16777215', + 'OperationError'); + } + }, + required: true, + }, + { + key: 'memory', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V < 8 * dict.parallelism) { + throw lazyDOMException( + 'memory must be at least 8 times the degree of parallelism', + 'OperationError'); + } + }, + required: true, + }, + { + key: 'passes', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V) => { + if (V === 0) { + throw lazyDOMException('passes must be > 0', 'OperationError'); + } + }, + required: true, + }, + { + key: 'version', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V, dict) => { + if (V !== 0x13) { + throw lazyDOMException( + `${V} is not a valid Argon2 version`, + 'OperationError'); + } + }, + }, + { + key: 'secretValue', + converter: converters.BufferSource, + }, + { + key: 'associatedData', + converter: converters.BufferSource, + }, + ], ]); function validateMacKeyLength(parameterName, zeroError) { @@ -847,34 +675,88 @@ function validateMacKeyLength(parameterName, zeroError) { for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ['KmacImportParams', 'DataError']]) { converters[name] = createDictionaryConverter( name, [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'length', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: validateMacKeyLength(`${name}.length`, zeroError), - }, + dictAlgorithm, + [ + { + key: 'length', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: validateMacKeyLength(`${name}.length`, zeroError), + }, + ], ]); } converters.KmacParams = createDictionaryConverter( 'KmacParams', [ - ...new SafeArrayIterator(dictAlgorithm), - { - key: 'outputLength', - converter: (V, opts) => - converters['unsigned long'](V, { ...opts, enforceRange: true }), - validator: (V, opts) => { - // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. - if (V % 8) - throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); - }, - required: true, - }, - { - key: 'customization', - converter: converters.BufferSource, - }, + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + // The Web Crypto spec allows for KMAC output length that are not multiples of 8. We don't. + if (V % 8) + throw lazyDOMException('Unsupported KmacParams outputLength', 'NotSupportedError'); + }, + required: true, + }, + { + key: 'customization', + converter: converters.BufferSource, + }, + ], + ]); + +converters.KangarooTwelveParams = createDictionaryConverter( + 'KangarooTwelveParams', [ + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); + }, + required: true, + }, + { + key: 'customization', + converter: converters.BufferSource, + }, + ], + ]); + +converters.TurboShakeParams = createDictionaryConverter( + 'TurboShakeParams', [ + dictAlgorithm, + [ + { + key: 'outputLength', + converter: (V, opts) => + converters['unsigned long'](V, { ...opts, enforceRange: true }), + validator: (V, opts) => { + if (V === 0 || V % 8) + throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); + }, + required: true, + }, + { + key: 'domainSeparation', + converter: (V, opts) => + converters.octet(V, { ...opts, enforceRange: true }), + validator: (V) => { + if (V < 0x01 || V > 0x7F) { + throw lazyDOMException( + 'TurboShakeParams.domainSeparation must be in range 0x01-0x7f', + 'OperationError'); + } + }, + }, + ], ]); module.exports = { diff --git a/lib/internal/crypto/x509.js b/lib/internal/crypto/x509.js index fcec607fb648de..cd5b5457e3ca67 100644 --- a/lib/internal/crypto/x509.js +++ b/lib/internal/crypto/x509.js @@ -18,6 +18,8 @@ const { const { PublicKeyObject, + getKeyObjectHandle, + getKeyObjectType, isKeyObject, } = require('internal/crypto/keys'); @@ -374,17 +376,17 @@ class X509Certificate { checkPrivateKey(pkey) { if (!isKeyObject(pkey)) throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); - if (pkey.type !== 'private') + if (getKeyObjectType(pkey) !== 'private') throw new ERR_INVALID_ARG_VALUE('pkey', pkey); - return this[kHandle].checkPrivateKey(pkey[kHandle]); + return this[kHandle].checkPrivateKey(getKeyObjectHandle(pkey)); } verify(pkey) { if (!isKeyObject(pkey)) throw new ERR_INVALID_ARG_TYPE('pkey', 'KeyObject', pkey); - if (pkey.type !== 'public') + if (getKeyObjectType(pkey) !== 'public') throw new ERR_INVALID_ARG_VALUE('pkey', pkey); - return this[kHandle].verify(pkey[kHandle]); + return this[kHandle].verify(getKeyObjectHandle(pkey)); } toLegacyObject() { diff --git a/lib/internal/locks.js b/lib/internal/locks.js index 05000e933f0b55..817159c06e281a 100644 --- a/lib/internal/locks.js +++ b/lib/internal/locks.js @@ -43,7 +43,7 @@ const kConstructLock = Symbol('kConstructLock'); const kConstructLockManager = Symbol('kConstructLockManager'); // WebIDL dictionary LockOptions -const convertLockOptions = createDictionaryConverter([ +const convertLockOptions = createDictionaryConverter('LockOptions', [ { key: 'mode', converter: createEnumConverter('LockMode', [ diff --git a/lib/internal/perf/performance.js b/lib/internal/perf/performance.js index ef2b5a55dd7c44..a12be66333d4f9 100644 --- a/lib/internal/perf/performance.js +++ b/lib/internal/perf/performance.js @@ -44,7 +44,7 @@ const timerify = require('internal/perf/timerify'); const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject } = require('internal/util'); const { inspect } = require('util'); const { validateThisInternalField } = require('internal/validators'); -const { convertToInt } = require('internal/webidl'); +const { converters } = require('internal/webidl'); const kPerformanceBrand = Symbol('performance'); @@ -144,8 +144,10 @@ class Performance extends EventTarget { if (arguments.length === 0) { throw new ERR_MISSING_ARGS('maxSize'); } - // unsigned long - maxSize = convertToInt('maxSize', maxSize, 32); + maxSize = converters['unsigned long']( + maxSize, + { __proto__: null, context: 'maxSize' }, + ); return setResourceTimingBufferSize(maxSize); } diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js index 36c134e82615b1..21489238801200 100644 --- a/lib/internal/quic/quic.js +++ b/lib/internal/quic/quic.js @@ -95,6 +95,8 @@ const { } = require('internal/blob'); const { + getKeyObjectHandle, + getKeyObjectType, isKeyObject, } = require('internal/crypto/keys'); @@ -138,7 +140,6 @@ const { kTrailers, kVersionNegotiation, kInspect, - kKeyObjectHandle, kWantsHeaders, kWantsTrailers, } = require('internal/quic/symbols'); @@ -2044,11 +2045,11 @@ function processIdentityOptions(identity, label) { const keyInputs = ArrayIsArray(keys) ? keys : [keys]; for (const key of keyInputs) { if (isKeyObject(key)) { - if (key.type !== 'private') { + if (getKeyObjectType(key) !== 'private') { throw new ERR_INVALID_ARG_VALUE(`${label}.keys`, key, 'must be a private key'); } - ArrayPrototypePush(keyHandles, key[kKeyObjectHandle]); + ArrayPrototypePush(keyHandles, getKeyObjectHandle(key)); } else { throw new ERR_INVALID_ARG_TYPE(`${label}.keys`, 'KeyObject', key); } diff --git a/lib/internal/util/comparisons.js b/lib/internal/util/comparisons.js index 6c65415a734753..23872fe9985cc1 100644 --- a/lib/internal/util/comparisons.js +++ b/lib/internal/util/comparisons.js @@ -127,10 +127,13 @@ const { getOwnNonIndexProperties, } = internalBinding('util'); -let kKeyObject; -let kExtractable; -let kAlgorithm; -let kKeyUsages; +let getCryptoKeyHandle; +let getCryptoKeyType; +let getCryptoKeyExtractable; +let getCryptoKeyAlgorithm; +let getCryptoKeyUsagesMask; +let getKeyObjectHandle; +let getKeyObjectType; const kStrict = 2; const kStrictWithoutPrototypes = 3; @@ -409,19 +412,34 @@ function objectComparisonStart(val1, val2, mode, memos) { return false; } } else if (isKeyObject(val1)) { - if (!isKeyObject(val2) || !val1.equals(val2)) { + if (getKeyObjectHandle === undefined) { + ({ + getKeyObjectHandle, + getKeyObjectType, + } = require('internal/crypto/keys')); + } + if (!isKeyObject(val2) || + getKeyObjectType(val1) !== getKeyObjectType(val2) || + !getKeyObjectHandle(val1).equals(getKeyObjectHandle(val2)) + ) { return false; } } else if (isCryptoKey(val1)) { - if (kKeyObject === undefined) { - kKeyObject = require('internal/crypto/util').kKeyObject; - ({ kExtractable, kAlgorithm, kKeyUsages } = require('internal/crypto/keys')); + if (getCryptoKeyHandle === undefined) { + ({ + getCryptoKeyHandle, + getCryptoKeyType, + getCryptoKeyExtractable, + getCryptoKeyAlgorithm, + getCryptoKeyUsagesMask, + } = require('internal/crypto/keys')); } if (!isCryptoKey(val2) || - val1[kExtractable] !== val2[kExtractable] || - !innerDeepEqual(val1[kAlgorithm], val2[kAlgorithm], mode, memos) || - !innerDeepEqual(val1[kKeyUsages], val2[kKeyUsages], mode, memos) || - !innerDeepEqual(val1[kKeyObject], val2[kKeyObject], mode, memos) + getCryptoKeyType(val1) !== getCryptoKeyType(val2) || + getCryptoKeyExtractable(val1) !== getCryptoKeyExtractable(val2) || + !innerDeepEqual(getCryptoKeyAlgorithm(val1), getCryptoKeyAlgorithm(val2), mode, memos) || + getCryptoKeyUsagesMask(val1) !== getCryptoKeyUsagesMask(val2) || + !getCryptoKeyHandle(val1).equals(getCryptoKeyHandle(val2)) ) { return false; } diff --git a/lib/internal/webidl.js b/lib/internal/webidl.js index 36bde94013d1a8..f727ea84a40535 100644 --- a/lib/internal/webidl.js +++ b/lib/internal/webidl.js @@ -2,98 +2,186 @@ const { ArrayBufferIsView, - ArrayBufferPrototypeGetByteLength, + ArrayBufferPrototypeGetResizable, + ArrayIsArray, ArrayPrototypePush, ArrayPrototypeToSorted, + BigInt, DataViewPrototypeGetBuffer, + FunctionPrototypeCall, MathAbs, MathMax, MathMin, MathPow, MathSign, MathTrunc, + Number, + NumberIsFinite, NumberIsNaN, NumberMAX_SAFE_INTEGER, NumberMIN_SAFE_INTEGER, - ObjectAssign, ObjectPrototypeIsPrototypeOf, + SafeArrayIterator, SafeSet, String, - Symbol, SymbolIterator, TypeError, TypedArrayPrototypeGetBuffer, + TypedArrayPrototypeGetSymbolToStringTag, } = primordials; +const { kEmptyObject, setOwnProperty } = require('internal/util'); const { - codes: { - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - }, -} = require('internal/errors'); -const { kEmptyObject } = require('internal/util'); -const { - isArrayBuffer, + isSharedArrayBuffer, isTypedArray, } = require('internal/util/types'); +const BIGINT_2_63 = 1n << 63n; +const BIGINT_2_64 = 1n << 64n; + const converters = { __proto__: null }; -const UNDEFINED = Symbol('undefined'); -const BOOLEAN = Symbol('boolean'); -const STRING = Symbol('string'); -const SYMBOL = Symbol('symbol'); -const NUMBER = Symbol('number'); -const BIGINT = Symbol('bigint'); -const NULL = Symbol('null'); -const OBJECT = Symbol('object'); +/** + * @typedef {object} ConversionOptions + * @property {string} [prefix] Message prefix for operation failures. + * @property {string} [context] Message context for the converted value. + * @property {string} [code] Node.js error code to assign to TypeError. + * @property {boolean} [enforceRange] Web IDL [EnforceRange] attribute. + * @property {boolean} [clamp] Web IDL [Clamp] attribute. + * @property {boolean} [allowShared] Web IDL [AllowShared] attribute for + * buffer view types. + * @property {boolean} [allowResizable] Web IDL [AllowResizable] attribute. + */ /** - * @see https://webidl.spec.whatwg.org/#es-any - * @param {any} V + * @callback Converter + * @param {any} V JavaScript value to convert to an IDL value. + * @param {ConversionOptions} [options] Conversion options. * @returns {any} */ -converters.any = (V) => { - return V; -}; -converters.object = (V, opts = kEmptyObject) => { - if (type(V) !== OBJECT) { - throw makeException( - 'is not an object', - kEmptyObject, - ); +/** + * @callback DictionaryMemberValidator + * @param {any} idlMemberValue Converted IDL member value. + * @param {object} jsDict Original JavaScript dictionary object. + * @returns {void} + */ + +/** + * @typedef {object} DictionaryMember + * @property {string} key Dictionary member identifier. + * @property {Converter} converter Converter for the member type. + * @property {boolean} [required] Whether the member is required. + * @property {() => any} [defaultValue] Function returning the default value. + * @property {DictionaryMemberValidator} [validator] Optional Node.js + * extension point invoked after conversion and before storing the member. + * This is for early semantic validation of known unsupported IDL values, + * especially in Web Crypto, where SubtleCrypto.supports() needs to answer + * from normalized dictionaries without running the requested operation. + */ + +/** + * Creates a TypeError with a Node.js error code. + * @param {string} message Error message. + * @param {string} code Node.js error code to assign. + * @returns {TypeError} + */ +function codedTypeError(message, code) { + // eslint-disable-next-line no-restricted-syntax + const err = new TypeError(message); + setOwnProperty(err, 'code', code); + return err; +} + +/** + * Creates the exception thrown by Web IDL converters. + * @param {string} message Unprefixed conversion failure message. + * @param {ConversionOptions} [options] Conversion options. + * @returns {TypeError} + */ +function makeException(message, options = kEmptyObject) { + const prefix = options.prefix ? options.prefix + ': ' : ''; + const context = options.context?.length === 0 ? + '' : (options.context ?? 'Value') + ' '; + return codedTypeError( + `${prefix}${context}${message}`, + options.code || 'ERR_INVALID_ARG_TYPE', + ); +} + +/** + * Returns the ECMAScript specification type of a JavaScript value. + * @see https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values + * @param {any} V JavaScript value. + * @returns {'Undefined'|'Null'|'Boolean'|'String'|'Symbol'|'Number'|'BigInt'|'Object'} + */ +function type(V) { + // ECMA-262 6.1: map JavaScript values to language type names. + switch (typeof V) { + case 'undefined': + return 'Undefined'; + case 'boolean': + return 'Boolean'; + case 'string': + return 'String'; + case 'symbol': + return 'Symbol'; + case 'number': + return 'Number'; + case 'bigint': + return 'BigInt'; + case 'object': + case 'function': + default: + // ECMA-262 6.1.2: null is its own language type. + // ECMA-262 6.1.7: functions are Object values. + if (V === null) { + return 'Null'; + } + return 'Object'; } - return V; -}; +} -// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart -const integerPart = MathTrunc; +/** + * Returns IntegerPart(n). + * @see https://webidl.spec.whatwg.org/#abstract-opdef-integerpart + * @param {number} n Numeric value. + * @returns {number} + */ +function integerPart(n) { + // Web IDL IntegerPart steps 1-3: floor(abs(n)), restore the sign, + // and choose +0 rather than -0. + const integer = MathTrunc(n); + return integer === 0 ? 0 : integer; +} -/* eslint-disable node-core/non-ascii-character */ -// Round x to the nearest integer, choosing the even integer if it lies halfway -// between two, and choosing +0 rather than -0. -// This is different from Math.round, which rounds to the next integer in the -// direction of +∞ when the fraction portion is exactly 0.5. -/* eslint-enable node-core/non-ascii-character */ +/** + * Rounds to the nearest integer, choosing the even integer on ties. + * @param {number} x Numeric value. + * @returns {number} + */ function evenRound(x) { - // Convert -0 to +0. - const i = integerPart(x) + 0; - const reminder = MathAbs(x % 1); - const sign = MathSign(i); - if (reminder === 0.5) { + // Web IDL ConvertToInt step 7.2: round to the nearest integer, + // choosing the even integer on ties and +0 rather than -0. + const i = integerPart(x); + const remainder = MathAbs(x % 1); + const sign = MathSign(x); + if (remainder === 0.5) { return i % 2 === 0 ? i : i + sign; } - const r = reminder < 0.5 ? i : i + sign; - // Convert -0 to +0. + const r = remainder < 0.5 ? i : i + sign; if (r === 0) { return 0; } return r; } +/** + * Returns 2 to the power of the given exponent. + * @param {number} exponent Non-negative integer exponent. + * @returns {number} + */ function pow2(exponent) { - // << operates on 32 bit signed integers. if (exponent < 31) { return 1 << exponent; } @@ -106,354 +194,877 @@ function pow2(exponent) { return MathPow(2, exponent); } -// https://tc39.es/ecma262/#eqn-modulo -// The notation “x modulo y” computes a value k of the same sign as y. +/** + * Returns x modulo y for Web IDL ConvertToInt step 10. + * + * This is intentionally not a general modulo helper. ConvertToInt only calls + * it with a positive power-of-two modulus, and the implementation assumes + * that. It converts JavaScript remainder into mathematical modulo and + * normalizes -0 to +0. + * @param {number} x Dividend. + * @param {number} y Positive divisor. + * @returns {number} + */ function modulo(x, y) { + // Web IDL ConvertToInt step 10 uses mathematical modulo. const r = x % y; - // Convert -0 to +0. if (r === 0) { return 0; } - return r; + return r > 0 ? r : r + y; +} + +/** + * Returns x modulo y for Web IDL ConvertToInt step 10. + * + * This is intentionally not a general modulo helper. ConvertToInt only calls + * it with a positive power-of-two modulus, and the implementation assumes + * that. BigInt has no -0, but this mirrors modulo()'s mathematical modulo + * behavior for the 64-bit path. + * @param {bigint} x Dividend. + * @param {bigint} y Positive divisor. + * @returns {bigint} + */ +function bigIntModulo(x, y) { + // Web IDL ConvertToInt step 10 uses mathematical modulo. + const r = x % y; + return r >= 0n ? r : r + y; } -// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -function convertToInt(name, value, bitLength, options = kEmptyObject) { - const { signed = false, enforceRange = false, clamp = false } = options; +/** + * Returns ToNumber(V). + * @see https://tc39.es/ecma262/#sec-tonumber + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {number} + */ +function toNumber(V, options = kEmptyObject) { + if (typeof V === 'bigint') { + // ECMA-262 ToNumber step 2: BigInt values throw. + throw makeException( + 'is a BigInt and cannot be converted to a number.', + options); + } + if (typeof V === 'symbol') { + // ECMA-262 ToNumber step 2: Symbol values throw. + throw makeException( + 'is a Symbol and cannot be converted to a number.', + options); + } + // Unary plus performs ToNumber, including ToPrimitive(V, number) for + // objects. Number(V) is not equivalent because it converts BigInt values, + // including BigInt values produced by ToPrimitive. + // Abrupt completions and native TypeErrors propagate unchanged. This is an + // intentional diagnostics tradeoff: decorating object conversion failures + // would require maintaining local ECMA-262 ToPrimitive and + // OrdinaryToPrimitive implementations. + return +V; +} + +/** + * Returns ToString(V). + * @see https://tc39.es/ecma262/#sec-tostring + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {string} + */ +function toString(V, options = kEmptyObject) { + if (typeof V === 'symbol') { + // ECMA-262 ToString step 2: Symbol values throw. + throw makeException( + 'is a Symbol and cannot be converted to a string.', + options); + } + + // The String function performs ToString for all non-Symbol primitives and + // objects, including ToPrimitive(V, string). String concatenation is not + // equivalent because it uses ToPrimitive(V, default). Abrupt completions + // and native TypeErrors propagate unchanged. This is an intentional + // diagnostics tradeoff: decorating object conversion failures would require + // maintaining local ECMA-262 ToPrimitive and OrdinaryToPrimitive + // implementations. + return String(V); +} + +/** + * Converts a JavaScript value to a Web IDL integer value. + * @see https://webidl.spec.whatwg.org/#abstract-opdef-converttoint + * @param {any} V JavaScript value. + * @param {number} bitLength Integer bit length. + * @param {'signed'|'unsigned'} [signedness] Integer signedness. + * @param {ConversionOptions} [options] Conversion options. + * @returns {number} + */ +function convertToInt( + V, + bitLength, + signedness = 'unsigned', + options = kEmptyObject, +) { + const signed = signedness === 'signed'; let upperBound; let lowerBound; - // 1. If bitLength is 64, then: + + // Web IDL ConvertToInt steps 1-3: determine lower/upper bounds. if (bitLength === 64) { - // 1.1. Let upperBound be 2^53 − 1. + // Steps 1.1-1.3 set upperBound to 2^53 - 1 and lowerBound to 0 + // for unsigned, or -2^53 + 1 for signed. This ensures 64-bit + // integer types associated with [EnforceRange] or [Clamp] are + // representable in JavaScript's Number type as unambiguous integers. upperBound = NumberMAX_SAFE_INTEGER; - // 1.2. If signedness is "unsigned", then let lowerBound be 0. - // 1.3. Otherwise let lowerBound be −2^53 + 1. - lowerBound = !signed ? 0 : NumberMIN_SAFE_INTEGER; + lowerBound = signed ? NumberMIN_SAFE_INTEGER : 0; } else if (!signed) { - // 2. Otherwise, if signedness is "unsigned", then: - // 2.1. Let lowerBound be 0. - // 2.2. Let upperBound be 2^bitLength − 1. + // Spell out the common Web IDL integer sizes so hot converters avoid + // recomputing powers of two on every call. lowerBound = 0; - upperBound = pow2(bitLength) - 1; + if (bitLength === 8) { + upperBound = 0xff; + } else if (bitLength === 16) { + upperBound = 0xffff; + } else if (bitLength === 32) { + upperBound = 0xffff_ffff; + } else { + upperBound = pow2(bitLength) - 1; + } + } else if (bitLength === 8) { + // Signed 8/16/32-bit conversions are mostly exercised through direct + // convertToInt() calls, but keep their common bounds cheap too. + lowerBound = -0x80; + upperBound = 0x7f; + } else if (bitLength === 16) { + lowerBound = -0x8000; + upperBound = 0x7fff; + } else if (bitLength === 32) { + lowerBound = -0x8000_0000; + upperBound = 0x7fff_ffff; } else { - // 3. Otherwise: - // 3.1. Let lowerBound be -2^(bitLength − 1). - // 3.2. Let upperBound be 2^(bitLength − 1) − 1. lowerBound = -pow2(bitLength - 1); upperBound = pow2(bitLength - 1) - 1; } - // 4. Let x be ? ToNumber(V). - let x = +value; - // 5. If x is −0, then set x to +0. + // Common case: primitive Number values that already fit the Web IDL + // range and have no fractional part are returned unchanged by every + // ConvertToInt path, except that -0 must become +0. This skips the + // generic ToNumber and option handling without skipping observable + // object coercion. + let x; + if (typeof V === 'number') { + // For primitive Numbers, in-range non-[Clamp] conversion is either + // identity or IntegerPart(V). This keeps the default and [EnforceRange] + // paths out of the generic ToNumber/options flow. + if (V >= lowerBound && V <= upperBound) { + const integer = MathTrunc(V); + if (integer === V) { + return V === 0 ? 0 : V; + } + if (options !== kEmptyObject && options.clamp) { + return evenRound(V); + } + return integer === 0 ? 0 : integer; + } + if (options !== kEmptyObject && options.enforceRange) { + // Keep [EnforceRange] ahead of [Clamp] without falling through to + // the shared check, which would observe options.enforceRange again. + if (!NumberIsFinite(V)) { + throw makeException( + 'is not a finite number.', + options); + } + + const integer = integerPart(V); + if (integer < lowerBound || integer > upperBound) { + throw makeException( + `is outside the expected range of ${lowerBound} to ${upperBound}.`, + { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); + } + + return integer; + } + if (options !== kEmptyObject && options.clamp && !NumberIsNaN(V)) { + // Out-of-range [Clamp] returns one of the already-computed bounds. + if (V <= lowerBound) { + return lowerBound === 0 ? 0 : lowerBound; + } + if (V >= upperBound) { + return upperBound === 0 ? 0 : upperBound; + } + } + x = V; + } else { + // Step 4: convert V with ECMA-262 ToNumber. + x = toNumber(V, options); + } + // Step 5: normalize -0 to +0. if (x === 0) { x = 0; } - // 6. If the conversion is to an IDL type associated with the [EnforceRange] - // extended attribute, then: - if (enforceRange) { - // 6.1. If x is NaN, +∞, or −∞, then throw a TypeError. - if (NumberIsNaN(x) || x === Infinity || x === -Infinity) { - throw new ERR_INVALID_ARG_VALUE(name, x); + // Step 6: [EnforceRange] rejects non-finite and out-of-range values. + if (options.enforceRange) { + // Step 6.1: reject NaN and infinities. + if (!NumberIsFinite(x)) { + throw makeException( + 'is not a finite number.', + options); } - // 6.2. Set x to IntegerPart(x). + + // Step 6.2: truncate to IntegerPart(x). x = integerPart(x); - // 6.3. If x < lowerBound or x > upperBound, then throw a TypeError. + // Steps 6.3-6.4: reject out-of-range values, otherwise return. if (x < lowerBound || x > upperBound) { - throw new ERR_INVALID_ARG_VALUE(name, x); + throw makeException( + `is outside the expected range of ${lowerBound} to ${upperBound}.`, + { __proto__: null, ...options, code: 'ERR_OUT_OF_RANGE' }); } - // 6.4. Return x. return x; } - // 7. If x is not NaN and the conversion is to an IDL type associated with - // the [Clamp] extended attribute, then: - if (clamp && !NumberIsNaN(x)) { - // 7.1. Set x to min(max(x, lowerBound), upperBound). + // Step 7: [Clamp] clamps, rounds, and returns non-NaN values. + if (options.clamp && !NumberIsNaN(x)) { + // Step 7.1: clamp x into the supported bounds. x = MathMin(MathMax(x, lowerBound), upperBound); + // Steps 7.2-7.3: round ties to even and return. + return evenRound(x); + } - // 7.2. Round x to the nearest integer, choosing the even integer if it - // lies halfway between two, and choosing +0 rather than −0. - x = evenRound(x); + // Step 8: NaN, +0, -0, and infinities become +0. + if (!NumberIsFinite(x) || x === 0) { + return 0; + } + + // Step 9: truncate to IntegerPart(x). + x = integerPart(x); - // 7.3. Return x. + // Steps 10-12 are an identity for values already in the step 1-3 + // bounds. For 64-bit conversions this only skips the safe-integer + // subset; values outside it still need exact BigInt modulo and the + // final Number approximation. + if (x >= lowerBound && x <= upperBound) { return x; } - // 8. If x is NaN, +0, +∞, or −∞, then return +0. - if (NumberIsNaN(x) || x === 0 || x === Infinity || x === -Infinity) { - return 0; + if (bitLength === 64) { + // Steps 10-12 still wrap over the full 2^64 IDL integer range. + // BigInt keeps x modulo 2^64 and the signed high-bit adjustment exact + // before this helper returns the JavaScript binding result. + let xBigInt = BigInt(x); + xBigInt = bigIntModulo(xBigInt, BIGINT_2_64); + + // For long long and unsigned long long values outside the safe-integer + // range, Web IDL says the JS Number value represents the closest numeric + // value, choosing the value with an even significand if there are two + // equally close values. Number(BigInt) performs that final approximation. + + // Step 11: wrap into the signed range when the high bit is set. + if (signed && xBigInt >= BIGINT_2_63) { + return Number(xBigInt - BIGINT_2_64); + } + + // Step 12: return the unsigned value. + return Number(xBigInt); } - // 9. Set x to IntegerPart(x). - x = integerPart(x); + // For 8/16/32-bit conversions, bitwise operators perform the same + // power-of-two wrapping as Web IDL step 10 for finite integer Numbers. + // The shifts narrow the unsigned value into the signed range when needed. + if (bitLength === 8) { + return signed ? (x << 24) >> 24 : x & 0xff; + } + if (bitLength === 16) { + return signed ? (x << 16) >> 16 : x & 0xffff; + } + if (bitLength === 32) { + return signed ? x | 0 : x >>> 0; + } - // 10. Set x to x modulo 2^bitLength. - x = modulo(x, pow2(bitLength)); + // Step 10: reduce modulo 2^bitLength. + const twoToTheBitLength = pow2(bitLength); + x = modulo(x, twoToTheBitLength); - // 11. If signedness is "signed" and x ≥ 2^(bitLength − 1), then return x − - // 2^bitLength. + // Step 11: wrap into the signed range when the high bit is set. if (signed && x >= pow2(bitLength - 1)) { - return x - pow2(bitLength); + return x - twoToTheBitLength; } - // 12. Otherwise, return x. + // Step 12: return the unsigned value. return x; } /** - * @see https://webidl.spec.whatwg.org/#es-DOMString - * @param {any} V - * @returns {string} + * Creates a converter for a Web IDL integer type. + * @param {number} bitLength Integer bit length. + * @param {'signed'|'unsigned'} [signedness] Integer signedness. + * @returns {Converter} */ -converters.DOMString = function DOMString(V) { - if (typeof V === 'symbol') { - throw new ERR_INVALID_ARG_VALUE('value', V); - } +function createIntegerConverter(bitLength, signedness = 'unsigned') { + return (V, options = kEmptyObject) => { + // Integer conversion step 1 calls ConvertToInt; step 2 returns + // the IDL value with the same numeric value. + return convertToInt(V, bitLength, signedness, options); + }; +} - return String(V); +/** + * Converts a JavaScript value to the IDL boolean type. + * @see https://webidl.spec.whatwg.org/#es-boolean + * @param {any} V JavaScript value. + * @returns {boolean} + */ +converters.boolean = (V) => { + // Web IDL boolean steps 1-2: ToBoolean(V), then return the same + // truth value as an IDL boolean. + return !!V; }; -converters['sequence'] = createSequenceConverter(converters.object); +/** + * Converts a JavaScript value to the IDL object type. + * @see https://webidl.spec.whatwg.org/#es-object + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {object|Function} + */ +converters.object = (V, options = kEmptyObject) => { + // Web IDL object step 1: throw unless V is an ECMA-262 Object. + if (type(V) !== 'Object') { + throw makeException( + 'is not an object.', + options, + ); + } + // Step 2: return a reference to the same object. + return V; +}; -function codedTypeError(message, errorProperties = kEmptyObject) { - // eslint-disable-next-line no-restricted-syntax - const err = new TypeError(message); - ObjectAssign(err, errorProperties); - return err; -} +/** + * Converts a JavaScript value to the IDL octet type. + * @see https://webidl.spec.whatwg.org/#es-octet + * @type {Converter} + */ +converters.octet = createIntegerConverter(8); -function makeException(message, opts = kEmptyObject) { - const prefix = opts.prefix ? opts.prefix + ': ' : ''; - const context = opts.context?.length === 0 ? - '' : (opts.context ?? 'Value') + ' '; - return codedTypeError( - `${prefix}${context}${message}`, - { code: opts.code || 'ERR_INVALID_ARG_TYPE' }, - ); +/** + * Converts a JavaScript value to the IDL unsigned short type. + * @see https://webidl.spec.whatwg.org/#es-unsigned-short + * @type {Converter} + */ +converters['unsigned short'] = createIntegerConverter(16); + +/** + * Converts a JavaScript value to the IDL unsigned long type. + * @see https://webidl.spec.whatwg.org/#es-unsigned-long + * @type {Converter} + */ +converters['unsigned long'] = createIntegerConverter(32); + +/** + * Converts a JavaScript value to the IDL long long type. + * @see https://webidl.spec.whatwg.org/#es-long-long + * @type {Converter} + */ +converters['long long'] = createIntegerConverter(64, 'signed'); + +/** + * Converts a JavaScript value to the IDL DOMString type. + * @see https://webidl.spec.whatwg.org/#es-DOMString + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {string} + */ +converters.DOMString = function DOMString(V, options = kEmptyObject) { + // Step 1 only applies to [LegacyNullToEmptyString], which this core + // converter does not implement. Steps 2-3 apply ToString(V) and + // return a DOMString with the same code units. + return toString(V, options); +}; + +/** + * Throws when a Web IDL operation receives too few arguments. + * @param {number} length Actual argument count. + * @param {number} required Required argument count. + * @param {ConversionOptions} [options] Conversion options. + * @returns {void} + */ +function requiredArguments(length, required, options = kEmptyObject) { + if (length < required) { + throw makeException( + `${required} argument${ + required === 1 ? '' : 's' + } required, but only ${length} present.`, + { __proto__: null, ...options, context: '', code: 'ERR_MISSING_ARGS' }); + } } +/** + * Creates a converter for a Web IDL enum type. + * @see https://webidl.spec.whatwg.org/#es-enumeration + * @param {string} name Enum identifier. + * @param {string[]} values Enum values. + * @returns {Converter} + */ function createEnumConverter(name, values) { - const E = new SafeSet(values); + const E = new SafeSet(new SafeArrayIterator(values)); - return function(V, opts = kEmptyObject) { - const S = String(V); + return function(V, options = kEmptyObject) { + // Web IDL enumeration step 1: convert V with ToString. + const S = toString(V, options); + // Step 2: throw unless S is one of the enumeration values. if (!E.has(S)) { throw makeException( - `value '${S}' is not a valid enum value of type ${name}.`, - { __proto__: null, ...opts, code: 'ERR_INVALID_ARG_VALUE' }); + `'${S}' is not a valid enum value of type ${name}.`, + { __proto__: null, ...options, code: 'ERR_INVALID_ARG_VALUE' }); } + // Step 3: return the matching enumeration value. return S; }; } -// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values -function type(V) { - switch (typeof V) { - case 'undefined': - return UNDEFINED; - case 'boolean': - return BOOLEAN; - case 'number': - return NUMBER; - case 'string': - return STRING; - case 'symbol': - return SYMBOL; - case 'bigint': - return BIGINT; - case 'object': // Fall through - case 'function': // Fall through - default: - if (V === null) { - return NULL; - } - // Per ES spec, typeof returns an implementation-defined value that is not - // any of the existing ones for uncallable non-standard exotic objects. - // Yet Type() which the Web IDL spec depends on returns Object for such - // cases. So treat the default case as an object. - return OBJECT; - } +/** + * Returns the context used when converting a dictionary member. + * @param {string} key Dictionary member identifier. + * @param {ConversionOptions} options Conversion options. + * @returns {string} + */ +function dictionaryMemberContext(key, options) { + return options.context ? `${key} in ${options.context}` : key; } -// https://webidl.spec.whatwg.org/#js-dictionary -function createDictionaryConverter(members) { - // The spec requires us to operate the members of a dictionary in - // lexicographical order. We are doing this in the outer scope to - // reduce the overhead that could happen in the returned function. - const sortedMembers = ArrayPrototypeToSorted(members, (a, b) => { +/** + * Returns the context used when converting a sequence element. + * @param {number} index Sequence element index. + * @param {ConversionOptions} options Conversion options. + * @returns {string} + */ +function sequenceElementContext(index, options) { + return `${options.context ?? 'Value'}[${index}]`; +} + +/** + * Returns the message used for a missing required dictionary member. + * @param {string} dictionaryName Dictionary identifier. + * @param {string} key Dictionary member identifier. + * @returns {string} + */ +function missingDictionaryMemberMessage(dictionaryName, key) { + return `cannot be converted to '${dictionaryName}' because ` + + `'${key}' is required in '${dictionaryName}'.`; +} + +/** + * Creates a converter for a Web IDL dictionary type. + * @see https://webidl.spec.whatwg.org/#js-dictionary + * @param {string} dictionaryName Dictionary identifier. + * @param {DictionaryMember[]|DictionaryMember[][]} members Dictionary members, + * either for a single dictionary or grouped from least-derived to + * most-derived dictionary. + * @returns {Converter} + */ +function createDictionaryConverter( + dictionaryName, + members, +) { + const compareMembers = (a, b) => { if (a.key === b.key) { return 0; } return a.key < b.key ? -1 : 1; - }); + }; + + const dictionaries = ArrayIsArray(members[0]) ? members : [members]; + const sortedDictionaries = []; + + // Web IDL dictionary conversion steps 3-4 process inherited dictionaries + // from least-derived to most-derived and sort only within each dictionary. + // Callers with inheritance pass one member array per dictionary level. + for (let i = 0; i < dictionaries.length; i++) { + ArrayPrototypePush( + sortedDictionaries, + ArrayPrototypeToSorted(dictionaries[i], compareMembers), + ); + } - return function( - V, - opts = kEmptyObject, - ) { - if (V != null && type(V) !== OBJECT) { + return function(jsDict, options = kEmptyObject) { + // Step 1: reject non-object, non-null, non-undefined values. + if (jsDict != null && type(jsDict) !== 'Object') { throw makeException( 'cannot be converted to a dictionary', - opts, + options, ); } + // Step 2: create the IDL dictionary value. const idlDict = { __proto__: null }; - for (let i = 0; i < sortedMembers.length; i++) { - const member = sortedMembers[i]; - const key = member.key; - let jsMemberValue; - if (V == null) { - jsMemberValue = undefined; - } else { - jsMemberValue = V[key]; - } - if (jsMemberValue !== undefined) { - const memberContext = opts.context ? `${key} in ${opts.context}` : `${key}`; - const converter = member.converter; - const idlMemberValue = converter( - jsMemberValue, - { - __proto__: null, - prefix: opts.prefix, - context: memberContext, - }, - ); - idlDict[key] = idlMemberValue; - } else if (typeof member.defaultValue === 'function') { - const idlMemberValue = member.defaultValue(); - idlDict[key] = idlMemberValue; - } else if (member.required) { - throw makeException( - `cannot be converted because of the missing '${key}'`, - opts, - ); + // Steps 3-4: iterate each dictionary level, then its sorted members. + for (let i = 0; i < sortedDictionaries.length; i++) { + const sortedMembers = sortedDictionaries[i]; + for (let j = 0; j < sortedMembers.length; j++) { + const member = sortedMembers[j]; + // Step 4.1.1: get the dictionary member identifier. + const key = member.key; + // Steps 4.1.2-4.1.3: read the JavaScript member value. + const jsMemberValue = jsDict == null ? undefined : jsDict[key]; + + // Step 4.1.4: convert and store present member values. + if (jsMemberValue !== undefined) { + const converter = member.converter; + // Step 4.1.4.1: convert the JavaScript value to IDL. + const idlMemberValue = converter( + jsMemberValue, + { + __proto__: null, + ...options, + context: dictionaryMemberContext(key, options), + }, + ); + // Validators are a Node.js extension after conversion. They let + // consumers reject known unsupported values while dictionary + // conversion still has precise member context. Web Crypto uses this + // so SubtleCrypto.supports() can make accurate decisions from + // normalized dictionaries instead of probing by running operations. + member.validator?.(idlMemberValue, jsDict); + // Step 4.1.4.2: set idlDict[key] to the IDL value. + idlDict[key] = idlMemberValue; + } else if (typeof member.defaultValue === 'function') { + // Step 4.1.5: store the member default value. + idlDict[key] = member.defaultValue(); + } else if (member.required) { + // Step 4.1.6: required missing members throw. + throw makeException( + missingDictionaryMemberMessage(dictionaryName, key), + { __proto__: null, ...options, code: 'ERR_MISSING_OPTION' }); + } } } + // Step 5: return the IDL dictionary. return idlDict; }; } -// https://webidl.spec.whatwg.org/#es-sequence +/** + * Creates a converter for a Web IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @param {Converter} converter Element converter. + * @returns {Converter} + */ function createSequenceConverter(converter) { - return function(V, opts = kEmptyObject) { - if (type(V) !== OBJECT) { + return function(V, options = kEmptyObject) { + // Web IDL sequence conversion step 1: require an ECMA-262 Object. + if (type(V) !== 'Object') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - const iter = V?.[SymbolIterator]?.(); - if (iter === undefined) { + + // Step 2: GetMethod(V, %Symbol.iterator%). + const method = V[SymbolIterator]; + // Step 3: throw if the iterator method is undefined, null, or not callable. + if (typeof method !== 'function') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - const array = []; + + // Step 4 and create-sequence step 1: get the iterator record. + const iterator = FunctionPrototypeCall(method, V); + const nextMethod = iterator?.next; + if (typeof nextMethod !== 'function') { + throw makeException( + 'cannot be converted to sequence.', + options); + } + + // Create-sequence step 2: initialize i to 0. + const idlSequence = []; while (true) { - const res = iter?.next?.(); - if (res === undefined) { + // Step 3.1: IteratorStepValue(iteratorRecord). + const next = FunctionPrototypeCall(nextMethod, iterator); + if (type(next) !== 'Object') { throw makeException( - 'can not be converted to sequence.', - opts); + 'cannot be converted to sequence.', + options); } - if (res.done === true) break; - const val = converter(res.value, { + // Step 3.2: IteratorComplete applies ToBoolean(done). + if (next.done) { + break; + } + // Step 3.3: convert next to an IDL value of type T. + const idlValue = converter(next.value, { __proto__: null, - ...opts, - context: `${opts.context}[${array.length}]`, + ...options, + context: sequenceElementContext(idlSequence.length, options), }); - ArrayPrototypePush(array, val); - }; - return array; + // Step 3.4: store the value and advance i. + ArrayPrototypePush(idlSequence, idlValue); + } + return idlSequence; }; } -// https://webidl.spec.whatwg.org/#js-interface -function createInterfaceConverter(name, I) { - return (V, opts = kEmptyObject) => { - // 1. If V implements I, then return the IDL interface type value that - // represents a reference to that platform object. - if (ObjectPrototypeIsPrototypeOf(I, V)) return V; - // 2. Throw a TypeError. - throw new ERR_INVALID_ARG_TYPE( - typeof opts.context === 'string' ? opts.context : 'value', name, V, - ); +/** + * Creates a converter for a Web IDL interface type. + * @see https://webidl.spec.whatwg.org/#js-interface + * @param {string} name Interface identifier. + * @param {object} prototype Interface prototype object. + * @returns {Converter} + */ +function createInterfaceConverter(name, prototype) { + return (V, options = kEmptyObject) => { + // Web IDL interface conversion step 1: return V if it implements I. + if (ObjectPrototypeIsPrototypeOf(prototype, V)) { + return V; + } + // Step 2: otherwise throw. + throw makeException( + `is not of type ${name}.`, + options); }; } -// Returns the [[ViewedArrayBuffer]] of an ArrayBufferView without leaving JS. +/** + * Returns the ArrayBuffer or SharedArrayBuffer viewed by a typed array or + * DataView. + * @param {ArrayBufferView} V TypedArray or DataView. + * @returns {ArrayBuffer|SharedArrayBuffer} + */ function getViewedArrayBuffer(V) { + // Buffer view conversion steps read V.[[ViewedArrayBuffer]]. return isTypedArray(V) ? TypedArrayPrototypeGetBuffer(V) : DataViewPrototypeGetBuffer(V); } -// Returns `true` if `buffer` is a `SharedArrayBuffer`. Uses a brand check via -// the `ArrayBuffer.prototype.byteLength` getter, which succeeds only on real -// (non-shared) ArrayBuffers and throws on SharedArrayBuffers — independent -// of the receiver's prototype chain. -function isSharedArrayBufferBacking(buffer) { +/** + * Validates [AllowShared] and [AllowResizable] backing-store constraints. + * @param {ArrayBuffer|SharedArrayBuffer} buffer Backing buffer. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateBufferSourceBacking(buffer, options) { + let resizable; try { - ArrayBufferPrototypeGetByteLength(buffer); - return false; + // ArrayBuffer.prototype.resizable is an ArrayBuffer brand check and the + // [AllowResizable] value we need. For SharedArrayBuffer-backed views it + // throws, which lets this path avoid a separate IsSharedArrayBuffer check. + // BufferSource has separate inline logic because [AllowShared] cannot be + // used with that typedef. + resizable = ArrayBufferPrototypeGetResizable(buffer); } catch { - return true; + // ArrayBufferView conversion step 2: reject SharedArrayBuffer + // backing stores unless [AllowShared] is present. + if (!options.allowShared) { + throw makeException( + 'is a view on a SharedArrayBuffer, which is not allowed.', + options); + } + // Step 3: reject non-fixed SharedArrayBuffer backing stores unless + // [AllowResizable] is present. + validateAllowGrowableSharedArrayBuffer(buffer, options); + return; + } + + // ArrayBuffer conversion step 3 and ArrayBufferView conversion step 3: + // reject non-fixed ArrayBuffer backing stores unless [AllowResizable] + // is present. + validateAllowResizableArrayBuffer(resizable, options); +} + +/** + * Validates the [AllowResizable] constraint for growable SharedArrayBuffer. + * @param {SharedArrayBuffer} buffer SharedArrayBuffer backing buffer. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateAllowGrowableSharedArrayBuffer(buffer, options) { + // SharedArrayBuffer and ArrayBufferView conversion step 3: + // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable]. + // Do not use a primordial getter here. When this module is included in the + // startup snapshot, an early-captured SharedArrayBuffer.prototype.growable + // getter does not detect growable buffers created after deserialization. + // Lazily capturing the getter would work, but it would observe the runtime + // prototype at first comparison, so it would not be an actual primordial. + if (!options.allowResizable && buffer.growable) { + throw makeException( + 'is backed by a growable SharedArrayBuffer, which is not allowed.', + options); } } -// https://webidl.spec.whatwg.org/#ArrayBufferView -converters.ArrayBufferView = (V, opts = kEmptyObject) => { - if (!ArrayBufferIsView(V)) { +/** + * Validates the [AllowResizable] constraint for resizable ArrayBuffer. + * @param {boolean} resizable ArrayBuffer [[ArrayBufferResizable]] value. + * @param {ConversionOptions} options Conversion options. + * @returns {void} + */ +function validateAllowResizableArrayBuffer(resizable, options) { + // ArrayBuffer and ArrayBufferView conversion step 3: + // IsFixedLengthArrayBuffer(buffer) must be true without [AllowResizable]. + // Read [[ArrayBufferResizable]] first so fixed buffers skip the options + // property lookup on this hot path. + if (resizable && !options.allowResizable) { throw makeException( - 'is not an ArrayBufferView.', - opts); + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); } - if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { +} + +/** + * Converts a JavaScript value to the IDL Uint8Array type. + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {Uint8Array} + */ +converters.Uint8Array = (V, options = kEmptyObject) => { + // Typed array conversion steps 1-2: T is Uint8Array, and V must + // have [[TypedArrayName]] equal to "Uint8Array". + if (!ArrayBufferIsView(V) || + TypedArrayPrototypeGetSymbolToStringTag(V) !== 'Uint8Array') { throw makeException( - 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); + 'is not an Uint8Array object.', + options); } + // Steps 3-4: validate [AllowShared] and [AllowResizable]. + validateBufferSourceBacking(TypedArrayPrototypeGetBuffer(V), options); + // Step 5: return a reference to the same object. return V; }; -// https://webidl.spec.whatwg.org/#BufferSource -converters.BufferSource = (V, opts = kEmptyObject) => { +/** + * Converts a JavaScript value to the IDL BufferSource typedef. + * @see https://webidl.spec.whatwg.org/#BufferSource + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {ArrayBuffer|ArrayBufferView} + */ +converters.BufferSource = (V, options = kEmptyObject) => { + // BufferSource is a typedef for (ArrayBufferView or ArrayBuffer). + // [AllowShared] cannot be used with BufferSource because the ArrayBuffer + // union branch does not support it. Use AllowSharedBufferSource instead. if (ArrayBufferIsView(V)) { - if (isSharedArrayBufferBacking(getViewedArrayBuffer(V))) { + const buffer = getViewedArrayBuffer(V); + // ArrayBufferView conversion steps 2-4: validate the viewed buffer + // and return a reference to the same view. + // Keep this logic inline instead of calling validateBufferSourceBacking(). + // BufferSource conversion is hot, and this avoids a helper call while still + // using a primordial getter for the backing-buffer internal-slot check. + // Unlike validateBufferSourceBacking(), this intentionally ignores + // options.allowShared because [AllowShared] cannot be used with + // BufferSource. + let resizable; + try { + // ArrayBuffer.prototype.resizable is both the ArrayBuffer brand check + // and the step 3 value. It throws for SharedArrayBuffer, which lets this + // path reject SAB-backed views without an extra byteLength getter call. + resizable = ArrayBufferPrototypeGetResizable(buffer); + } catch { throw makeException( 'is a view on a SharedArrayBuffer, which is not allowed.', - opts); + options); + } + if (resizable && !options.allowResizable) { + throw makeException( + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); } - return V; } - if (!isArrayBuffer(V)) { + // ArrayBuffer conversion steps 1-2: require a non-shared ArrayBuffer. + // Use the primordial resizable getter as both the ArrayBuffer brand check + // and the step 3 value. This avoids isArrayBuffer(V) followed by another + // getter call, and rejects SharedArrayBuffer on this union branch. + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(V); + } catch { throw makeException( 'is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.', - opts); + options); } + // ArrayBuffer conversion step 3: validate [AllowResizable]. + // ArrayBufferPrototypeGetResizable(V) already excluded SharedArrayBuffer, + // so no [AllowShared] validation is needed on this branch. + if (resizable && !options.allowResizable) { + throw makeException( + 'is backed by a resizable ArrayBuffer, which is not allowed.', + options); + } + + // Step 4: return a reference to the same ArrayBuffer. return V; }; +/** + * Converts a JavaScript value to the IDL AllowSharedBufferSource typedef. + * @see https://webidl.spec.whatwg.org/#AllowSharedBufferSource + * @param {any} V JavaScript value. + * @param {ConversionOptions} [options] Conversion options. + * @returns {ArrayBuffer|SharedArrayBuffer|ArrayBufferView} + */ +converters.AllowSharedBufferSource = (V, options = kEmptyObject) => { + // AllowSharedBufferSource is a typedef for + // (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView). + // The union branches are disjoint, so this keeps the hot view path first. + if (ArrayBufferIsView(V)) { + const buffer = getViewedArrayBuffer(V); + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(buffer); + } catch { + validateAllowGrowableSharedArrayBuffer(buffer, options); + return V; + } + validateAllowResizableArrayBuffer(resizable, options); + return V; + } + + let resizable; + try { + resizable = ArrayBufferPrototypeGetResizable(V); + } catch { + if (isSharedArrayBuffer(V)) { + validateAllowGrowableSharedArrayBuffer(V, options); + return V; + } + throw makeException( + 'is not instance of ArrayBuffer, SharedArrayBuffer, Buffer, ' + + 'TypedArray, or DataView.', + options); + } + + validateAllowResizableArrayBuffer(resizable, options); + return V; +}; + +/** + * Converts a JavaScript value to the IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @type {Converter} + */ +converters['sequence'] = + createSequenceConverter(converters.DOMString); + +/** + * Converts a JavaScript value to the IDL sequence type. + * @see https://webidl.spec.whatwg.org/#es-sequence + * @type {Converter} + */ +converters['sequence'] = + createSequenceConverter(converters.object); + module.exports = { - type, converters, convertToInt, + createDictionaryConverter, createEnumConverter, createInterfaceConverter, createSequenceConverter, - createDictionaryConverter, - evenRound, - makeException, + requiredArguments, + type, }; diff --git a/lib/internal/worker/js_transferable.js b/lib/internal/worker/js_transferable.js index 6acc0a3b19a3cc..0ae80d9a728725 100644 --- a/lib/internal/worker/js_transferable.js +++ b/lib/internal/worker/js_transferable.js @@ -100,6 +100,7 @@ function markTransferMode(obj, cloneable = false, transferable = false) { webidl.converters.StructuredSerializeOptions = webidl .createDictionaryConverter( + 'StructuredSerializeOptions', [ { key: 'transfer', diff --git a/node.gyp b/node.gyp index 27402b3061afba..2e35508f091dd1 100644 --- a/node.gyp +++ b/node.gyp @@ -382,10 +382,11 @@ 'src/crypto/crypto_cipher.cc', 'src/crypto/crypto_context.cc', 'src/crypto/crypto_ec.cc', - 'src/crypto/crypto_ml_dsa.cc', + 'src/crypto/crypto_pqc.cc', 'src/crypto/crypto_kem.cc', 'src/crypto/crypto_hmac.cc', 'src/crypto/crypto_kmac.cc', + 'src/crypto/crypto_turboshake.cc', 'src/crypto/crypto_random.cc', 'src/crypto/crypto_rsa.cc', 'src/crypto/crypto_spkac.cc', @@ -404,6 +405,7 @@ 'src/crypto/crypto_dh.h', 'src/crypto/crypto_hmac.h', 'src/crypto/crypto_kmac.h', + 'src/crypto/crypto_turboshake.h', 'src/crypto/crypto_rsa.h', 'src/crypto/crypto_spkac.h', 'src/crypto/crypto_util.h', @@ -418,7 +420,7 @@ 'src/crypto/crypto_clienthello.h', 'src/crypto/crypto_context.h', 'src/crypto/crypto_ec.h', - 'src/crypto/crypto_ml_dsa.h', + 'src/crypto/crypto_pqc.h', 'src/crypto/crypto_hkdf.h', 'src/crypto/crypto_pbkdf2.h', 'src/crypto/crypto_sig.h', diff --git a/src/crypto/README.md b/src/crypto/README.md index 3f4400f595d356..2850aca077eafc 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -149,24 +149,36 @@ core key objects. #### `KeyObjectData` `KeyObjectData` is an internal thread-safe structure used to wrap either -a `EVPKeyPointer` (for Public or Private keys) or a `ByteSource` containing -a Secret key. +an `EVPKeyPointer` (for Public or Private keys) or a `ByteSource` containing +a Secret key. It is the shared backing representation used by `KeyObject`, +`CryptoKey`, and native crypto jobs that operate on key material. #### `KeyObjectHandle` -The `KeyObjectHandle` provides the interface between the native C++ code -handling keys and the public JavaScript `KeyObject` API. +`KeyObjectHandle` is the internal JavaScript-visible C++ handle for a +`KeyObjectData`. It exposes operations that internal JavaScript uses to +initialize, inspect, compare, and export key material. Native code passes +`KeyObjectData` across threads and jobs; a `KeyObjectHandle` is created when +JavaScript needs access to those operations and is kept out of user-visible +`KeyObject` own properties. #### `KeyObject` -A `KeyObject` is the public Node.js-specific API for keys. A single -`KeyObject` wraps exactly one `KeyObjectHandle`. +A `KeyObject` is the public Node.js-specific API for keys. It extends a +native `NativeKeyObject`, which stores `KeyObjectData` for structured +cloning. The JavaScript API surface reads its key type and a +`KeyObjectHandle` through a hidden native-backed slot tuple, caching that +tuple in a private field outside user-visible own properties. Derived +metadata, such as symmetric key size and asymmetric key details, is read +from the cached handle and appended lazily to the same private-field cache. #### `CryptoKey` -A `CryptoKey` is the Web Crypto API's alternative to `KeyObject`. In the -Node.js implementation, `CryptoKey` is a thin wrapper around the -`KeyObject` and it is largely possible to use them interchangeably. +A `CryptoKey` is the Web Crypto API key type. In the Node.js implementation, +public `CryptoKey` instances are backed by a native `NativeCryptoKey`, not by +a `KeyObject`. `NativeCryptoKey` stores the same `KeyObjectData` +representation as `KeyObject`, plus the Web Crypto internal slots +(`[[extractable]]`, `[[algorithm]]`, and `[[usages]]`). ### `CryptoJob` @@ -174,7 +186,8 @@ All operations that are not either Stream-based or single-use functions are built around the `CryptoJob` class. A `CryptoJob` encapsulates a single crypto operation that can be -invoked synchronously or asynchronously. +invoked synchronously, asynchronously, or as a Web Crypto API +Promise-based job. The `CryptoJob` class itself is a C++ template that takes a single `CryptoJobTraits` struct as a parameter. The `CryptoJobTraits` @@ -218,14 +231,15 @@ specializations and will either be called synchronously within the current thread or from within the libuv threadpool. Every `CryptoJob` instance exposes a `run()` function to the -JavaScript layer. When called, `run()` with either dispatch the -job to the libuv threadpool or invoke the Implementation -function synchronously. If invoked synchronously, run() will -return a JavaScript array. The first value in the array is -either an `Error` or `undefined`. If the operation was successful, -the second value in the array will contain the result of the -operation. Typically, the result is an `ArrayBuffer`, but -certain `CryptoJob` types can alter the output. +JavaScript layer. When called, `run()` will either dispatch the +job to the libuv threadpool, invoke the Implementation function +synchronously, or return a `Promise` for Web Crypto API jobs. If +invoked synchronously, `run()` will return a JavaScript array. +The first value in the array is either an `Error` or `undefined`. +If the operation was successful, the second value in the array +will contain the result of the operation. Typically, the result +is an `ArrayBuffer`, but certain `CryptoJob` types can alter the +output. If the `CryptoJob` is processed asynchronously, then the job must have an `ondone` property whose value is a function that @@ -234,6 +248,12 @@ be called with two arguments. The first is either an `Error` or `undefined`, and the second is the result of the operation if successful. +If the `CryptoJob` is processed as a Web Crypto API job, then +`run()` returns a Promise. Operation-specific failures are +rejected with an `OperationError`, and successful jobs resolve +with the Web Crypto API result shape expected by the JavaScript +implementation. + For `CipherJob` types, the output is always an `ArrayBuffer`. For `KeyExportJob` types, the output is either an `ArrayBuffer` or @@ -241,7 +261,9 @@ a JavaScript object (for JWK output format); For `KeyGenJob` types, the output is either a single KeyObject, or an array containing a Public/Private key pair represented -either as a `KeyObjectHandle` object or a `Buffer`. +either as a `KeyObjectHandle` object or a `Buffer`. Web Crypto +API key generation jobs return a `CryptoKey` or a `CryptoKeyPair` +object. For `DeriveBitsJob` type output is typically an `ArrayBuffer` but can be other values (`RandomBytesJob` for instance, fills an @@ -266,11 +288,12 @@ should be used to throw JavaScript errors when necessary. ### Operation mode -All crypto functions in Node.js operate in one of three +All crypto functions in Node.js operate in one of these modes: * Synchronous single-call * Asynchronous single-call +* Web Crypto API Promise-based * Stream-oriented It is often possible to perform various operations across diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index fa619696ffd5b2..9172def7d4ebee 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -181,6 +181,68 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, return WebCryptoCipherStatus::OK; } +#ifdef OPENSSL_IS_BORINGSSL +// AES Key Wrap using BoringSSL's low-level AES_wrap_key / AES_unwrap_key. +// BoringSSL does not expose EVP_aes_*_wrap via the +// EVP_CIPHER registry, so the EVP-based AES_Cipher path is unusable for +// AES-KW. This matches Chromium's WebCrypto AES-KW implementation. +WebCryptoCipherStatus AES_KW_Cipher(Environment* env, + const KeyObjectData& key_data, + WebCryptoCipherMode cipher_mode, + const AESCipherConfig& params, + const ByteSource& in, + ByteSource* out) { + CHECK_EQ(key_data.GetKeyType(), kKeyTypeSecret); + + const unsigned key_bits = + static_cast(key_data.GetSymmetricKeySize()) * 8; + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const bool encrypt = cipher_mode == kWebCryptoCipherEncrypt; + + AES_KEY aes_key; + if (encrypt) { + // Input must be a multiple of 8 bytes and at least 16 bytes. + if (in.size() < 16 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_encrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() + 8); + int len = AES_wrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() + 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } else { + // Input must be a multiple of 8 bytes and at least 24 bytes. + if (in.size() < 24 || in.size() % 8 != 0) { + return WebCryptoCipherStatus::FAILED; + } + if (AES_set_decrypt_key(key_bytes, key_bits, &aes_key) != 0) { + return WebCryptoCipherStatus::FAILED; + } + auto buf = DataPointer::Alloc(in.size() - 8); + int len = AES_unwrap_key(&aes_key, + nullptr, + static_cast(buf.get()), + in.data(), + in.size()); + if (len < 0 || static_cast(len) != in.size() - 8) { + return WebCryptoCipherStatus::FAILED; + } + *out = ByteSource::Allocated(buf.release()); + } + + return WebCryptoCipherStatus::OK; +} +#endif // OPENSSL_IS_BORINGSSL + // The AES_CTR implementation here takes it's inspiration from the chromium // implementation here: // https://github.com/chromium/chromium/blob/7af6cfd/components/webcrypto/algorithms/aes_ctr.cc @@ -356,9 +418,7 @@ bool ValidateIV( THROW_ERR_OUT_OF_RANGE(env, "iv is too big"); return false; } - params->iv = (mode == kCryptoJobAsync) - ? iv.ToCopy() - : iv.ToByteSource(); + params->iv = (IsCryptoJobAsync(mode)) ? iv.ToCopy() : iv.ToByteSource(); return true; } @@ -404,9 +464,9 @@ bool ValidateAdditionalData( THROW_ERR_OUT_OF_RANGE(env, "additionalData is too big"); return false; } - params->additional_data = mode == kCryptoJobAsync - ? additional.ToCopy() - : additional.ToByteSource(); + params->additional_data = IsCryptoJobAsync(mode) + ? additional.ToCopy() + : additional.ToByteSource(); } return true; } @@ -433,7 +493,7 @@ AESCipherConfig& AESCipherConfig::operator=(AESCipherConfig&& other) noexcept { void AESCipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the AESCipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } @@ -465,6 +525,19 @@ Maybe AESCipherTraits::AdditionalConfig( } #undef V +#ifdef OPENSSL_IS_BORINGSSL + // On BoringSSL the KW variants have no backing EVP_CIPHER; they use + // low-level AES_wrap_key / AES_unwrap_key instead. + const bool is_kw = params->variant == AESKeyVariant::KW_128 || + params->variant == AESKeyVariant::KW_192 || + params->variant == AESKeyVariant::KW_256; + + if (is_kw) { + UseDefaultIV(params); + return JustVoid(); + } +#endif + if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 5627f9020bad54..401e7b2c338a1b 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -22,11 +22,23 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(GCM_128, AES_Cipher, ncrypto::Cipher::AES_128_GCM) \ V(GCM_192, AES_Cipher, ncrypto::Cipher::AES_192_GCM) \ V(GCM_256, AES_Cipher, ncrypto::Cipher::AES_256_GCM) \ + VARIANTS_KW(V) + +#ifdef OPENSSL_IS_BORINGSSL +// BoringSSL does not expose EVP_aes_*_wrap via the EVP_CIPHER registry. +// Route AES-KW through low-level AES_wrap_key / AES_unwrap_key instead. +#define VARIANTS_KW(V) \ + V(KW_128, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_192, AES_KW_Cipher, static_cast(nullptr)) \ + V(KW_256, AES_KW_Cipher, static_cast(nullptr)) +#else +#define VARIANTS_KW(V) \ V(KW_128, AES_Cipher, ncrypto::Cipher::AES_128_KW) \ V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) +#endif -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_AES_OCB #define VARIANTS_OCB(V) \ V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ diff --git a/src/crypto/crypto_argon2.cc b/src/crypto/crypto_argon2.cc index b3f74d157f8917..3dcee3fed88a04 100644 --- a/src/crypto/crypto_argon2.cc +++ b/src/crypto/crypto_argon2.cc @@ -1,9 +1,11 @@ #include "crypto/crypto_argon2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" +#include "memory_tracker-inl.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_NUMBER >= 0x30200000L -#ifndef OPENSSL_NO_ARGON2 +#if OPENSSL_WITH_ARGON2 #include namespace node::crypto { @@ -20,6 +22,7 @@ using v8::Value; Argon2Config::Argon2Config(Argon2Config&& other) noexcept : mode{other.mode}, + key{std::move(other.key)}, pass{std::move(other.pass)}, salt{std::move(other.salt)}, secret{std::move(other.secret)}, @@ -37,8 +40,9 @@ Argon2Config& Argon2Config::operator=(Argon2Config&& other) noexcept { } void Argon2Config::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("secret", secret.size()); tracker->TrackFieldWithSize("ad", ad.size()); @@ -60,14 +64,23 @@ Maybe Argon2Traits::AdditionalConfig( config->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); ArrayBufferOrViewContents secret(args[offset + 6]); ArrayBufferOrViewContents ad(args[offset + 7]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + config->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + config->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -85,8 +98,7 @@ Maybe Argon2Traits::AdditionalConfig( return Nothing(); } - const bool isAsync = mode == kCryptoJobAsync; - config->pass = isAsync ? pass.ToCopy() : pass.ToByteSource(); + const bool isAsync = IsCryptoJobAsync(mode); config->salt = isAsync ? salt.ToCopy() : salt.ToByteSource(); config->secret = isAsync ? secret.ToCopy() : secret.ToByteSource(); config->ad = isAsync ? ad.ToCopy() : ad.ToByteSource(); @@ -119,7 +131,13 @@ bool Argon2Traits::DeriveBits(Environment* env, } // Both the pass and salt may be zero-length at this point - auto dp = ncrypto::argon2(config.pass, + const ncrypto::Buffer pass{ + .data = config.key ? config.key.GetSymmetricKey() + : config.pass.data(), + .len = config.key ? config.key.GetSymmetricKeySize() : config.pass.size(), + }; + + auto dp = ncrypto::argon2(pass, config.salt, config.lanes, config.keylen, @@ -155,4 +173,3 @@ void Argon2::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto #endif -#endif diff --git a/src/crypto/crypto_argon2.h b/src/crypto/crypto_argon2.h index 4ba51435d88d1d..71f5def67b6599 100644 --- a/src/crypto/crypto_argon2.h +++ b/src/crypto/crypto_argon2.h @@ -3,10 +3,11 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" namespace node::crypto { -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 // Argon2 is a password-based key derivation algorithm // defined in https://datatracker.ietf.org/doc/html/rfc9106 @@ -22,6 +23,7 @@ namespace node::crypto { struct Argon2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; ByteSource secret; diff --git a/src/crypto/crypto_chacha20_poly1305.cc b/src/crypto/crypto_chacha20_poly1305.cc index 0fd3e0517317ca..cfe43122d5aa55 100644 --- a/src/crypto/crypto_chacha20_poly1305.cc +++ b/src/crypto/crypto_chacha20_poly1305.cc @@ -10,6 +10,9 @@ #include "v8.h" #include +#ifdef OPENSSL_IS_BORINGSSL +#include +#endif namespace node { @@ -45,7 +48,7 @@ bool ValidateIV(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->iv = iv.ToCopy(); } else { params->iv = iv.ToByteSource(); @@ -65,7 +68,7 @@ bool ValidateAdditionalData(Environment* env, return false; } - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { params->additional_data = additional_data.ToCopy(); } else { params->additional_data = additional_data.ToByteSource(); @@ -93,7 +96,7 @@ ChaCha20Poly1305CipherConfig& ChaCha20Poly1305CipherConfig::operator=( void ChaCha20Poly1305CipherConfig::MemoryInfo(MemoryTracker* tracker) const { // If mode is sync, then the data in each of these properties // is not owned by the ChaCha20Poly1305CipherConfig, so we ignore it. - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("iv", iv.size()); tracker->TrackFieldWithSize("additional_data", additional_data.size()); } @@ -110,10 +113,15 @@ Maybe ChaCha20Poly1305CipherTraits::AdditionalConfig( params->mode = mode; params->cipher = ncrypto::Cipher::CHACHA20_POLY1305; +#ifndef OPENSSL_IS_BORINGSSL + // On BoringSSL, ChaCha20-Poly1305 is not exposed via the EVP_CIPHER registry + // so FromNid() returns a null Cipher. We use EVP_AEAD directly in DoCipher + // instead. if (!params->cipher) { THROW_ERR_CRYPTO_UNKNOWN_CIPHER(env); return Nothing(); } +#endif // IV parameter (required) if (!ValidateIV(env, mode, args[offset], params)) { @@ -144,6 +152,75 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( return WebCryptoCipherStatus::INVALID_KEY_TYPE; } +#ifdef OPENSSL_IS_BORINGSSL + // BoringSSL does not expose ChaCha20-Poly1305 via the EVP_CIPHER registry; + // it is only available through the EVP_AEAD API. Matches Chromium's + // WebCrypto ChaCha20-Poly1305 implementation. + const auto key_bytes = + reinterpret_cast(key_data.GetSymmetricKey()); + const auto ad_bytes = params.additional_data.data(); + const auto ad_len = params.additional_data.size(); + const auto iv_bytes = params.iv.data(); + const auto iv_len = params.iv.size(); + + bssl::ScopedEVP_AEAD_CTX ctx; + if (!EVP_AEAD_CTX_init(ctx.get(), + EVP_aead_chacha20_poly1305(), + key_bytes, + key_data.GetSymmetricKeySize(), + kChaCha20Poly1305TagSize, + nullptr)) { + return WebCryptoCipherStatus::FAILED; + } + + if (cipher_mode == kWebCryptoCipherEncrypt) { + size_t out_len = 0; + const size_t max_out_len = in.size() + kChaCha20Poly1305TagSize; + auto buf = DataPointer::Alloc(max_out_len); + if (!EVP_AEAD_CTX_seal(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + return WebCryptoCipherStatus::OK; + } + + // Decrypt + if (in.size() < kChaCha20Poly1305TagSize) { + return WebCryptoCipherStatus::FAILED; + } + size_t out_len = 0; + const size_t max_out_len = in.size(); // at most |in_len| bytes written + auto buf = DataPointer::Alloc(max_out_len == 0 ? 1 : max_out_len); + if (!EVP_AEAD_CTX_open(ctx.get(), + static_cast(buf.get()), + &out_len, + max_out_len, + iv_bytes, + iv_len, + in.data(), + in.size(), + ad_bytes, + ad_len)) { + return WebCryptoCipherStatus::FAILED; + } + if (out_len == 0) { + *out = ByteSource(); + } else { + buf = buf.resize(out_len); + *out = ByteSource::Allocated(buf.release()); + } + return WebCryptoCipherStatus::OK; +#else auto ctx = CipherCtxPointer::New(); CHECK(ctx); @@ -242,6 +319,7 @@ WebCryptoCipherStatus ChaCha20Poly1305CipherTraits::DoCipher( *out = ByteSource::Allocated(buf.release()); return WebCryptoCipherStatus::OK; +#endif // OPENSSL_IS_BORINGSSL } void ChaCha20Poly1305::Initialize(Environment* env, Local target) { diff --git a/src/crypto/crypto_cipher.cc b/src/crypto/crypto_cipher.cc index ed509e5c9fe79c..46f5ca20367f66 100644 --- a/src/crypto/crypto_cipher.cc +++ b/src/crypto/crypto_cipher.cc @@ -432,61 +432,46 @@ bool CipherBase::InitAuthenticated(const char* cipher_type, return false; } - if (ctx_.isGcmMode()) { - if (auth_tag_len != kNoAuthTagLength) { - if (!Cipher::IsValidGCMTagLength(auth_tag_len)) { - THROW_ERR_CRYPTO_INVALID_AUTH_TAG( - env(), - "Invalid authentication tag length: %u", - auth_tag_len); - return false; - } - - // Remember the given authentication tag length for later. - auth_tag_len_ = auth_tag_len; - } - } else { - if (auth_tag_len == kNoAuthTagLength) { - // We treat ChaCha20-Poly1305 specially. Like GCM, the authentication tag - // length defaults to 16 bytes when encrypting. Unlike GCM, the - // authentication tag length also defaults to 16 bytes when decrypting, - // whereas GCM would accept any valid authentication tag length. - if (ctx_.isChaCha20Poly1305()) { - auth_tag_len = EVP_CHACHAPOLY_TLS_TAG_LEN; - } else { - THROW_ERR_CRYPTO_INVALID_AUTH_TAG( - env(), "authTagLength required for %s", cipher_type); - return false; - } - } - + if (ctx_.isCcmMode()) { // TODO(tniessen) Support CCM decryption in FIPS mode - - if (ctx_.isCcmMode() && kind_ == kDecipher && ncrypto::isFipsEnabled()) { - THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION(env(), - "CCM encryption not supported in FIPS mode"); + if (kind_ == kDecipher && ncrypto::isFipsEnabled()) { + THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION( + env(), "CCM encryption not supported in FIPS mode"); return false; } - // Tell OpenSSL about the desired length. - if (!ctx_.setAeadTagLength(auth_tag_len)) { + // Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes. + CHECK(iv_len >= 7 && iv_len <= 13); + max_message_size_ = INT_MAX; + if (iv_len == 12) max_message_size_ = 16777215; + if (iv_len == 13) max_message_size_ = 65535; + } + + if (auth_tag_len == kNoAuthTagLength) { + // GCM accepts any valid authentication tag length when decrypting without + // an explicit authTagLength. This remains deprecated, but supported. + if (ctx_.isGcmMode()) { + return true; +#ifdef EVP_CHACHAPOLY_TLS_TAG_LEN + } else if (ctx_.isChaCha20Poly1305()) { + auth_tag_len = EVP_CHACHAPOLY_TLS_TAG_LEN; +#endif + } else { THROW_ERR_CRYPTO_INVALID_AUTH_TAG( - env(), "Invalid authentication tag length: %u", auth_tag_len); + env(), "authTagLength required for %s", cipher_type); return false; } - - // Remember the given authentication tag length for later. - auth_tag_len_ = auth_tag_len; - - if (ctx_.isCcmMode()) { - // Restrict the message length to min(INT_MAX, 2^(8*(15-iv_len))-1) bytes. - CHECK(iv_len >= 7 && iv_len <= 13); - max_message_size_ = INT_MAX; - if (iv_len == 12) max_message_size_ = 16777215; - if (iv_len == 13) max_message_size_ = 65535; - } + } else if ((ctx_.isGcmMode() && !Cipher::IsValidGCMTagLength(auth_tag_len)) || + (!ctx_.isGcmMode() && !ctx_.setAeadTagLength(auth_tag_len))) { + // GCM authentication tag lengths are restricted according to NIST 800-38d, + // page 9. For other modes, we rely on OpenSSL to validate the length. + THROW_ERR_CRYPTO_INVALID_AUTH_TAG( + env(), "Invalid authentication tag length: %u", auth_tag_len); + return false; } + // Remember the given authentication tag length for later. + auth_tag_len_ = auth_tag_len; return true; } @@ -753,7 +738,7 @@ bool CipherBase::Final(std::unique_ptr* out) { static_cast(ctx_.getBlockSize()), BackingStoreInitializationMode::kUninitialized); -#if (OPENSSL_VERSION_NUMBER < 0x30000000L) +#if !OPENSSL_VERSION_PREREQ(3, 0) // OpenSSL v1.x doesn't verify the presence of the auth tag so do // it ourselves, see https://github.com/nodejs/node/issues/45874. if (kind_ == kDecipher && ctx_.isChaCha20Poly1305() && diff --git a/src/crypto/crypto_cipher.h b/src/crypto/crypto_cipher.h index 006d18a7118761..a00afa6a0f9f81 100644 --- a/src/crypto/crypto_cipher.h +++ b/src/crypto/crypto_cipher.h @@ -164,13 +164,7 @@ class CipherJob final : public CryptoJob { } new CipherJob( - env, - args.This(), - mode, - key, - cipher_mode, - data, - std::move(params)); + env, args.This(), mode, key, cipher_mode, data, std::move(params)); } static void Initialize( @@ -197,7 +191,7 @@ class CipherJob final : public CryptoJob { std::move(params)), key_(key->Data().addRef()), cipher_mode_(cipher_mode), - in_(mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource()) {} + in_(IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource()) {} const KeyObjectData& key() const { return key_; } @@ -261,7 +255,7 @@ class CipherJob final : public CryptoJob { SET_SELF_SIZE(CipherJob) void MemoryInfo(MemoryTracker* tracker) const override { - if (CryptoJob::mode() == kCryptoJobAsync) + if (IsCryptoJobAsync(CryptoJob::mode())) tracker->TrackFieldWithSize("in", in_.size()); tracker->TrackFieldWithSize("out", out_.size()); CryptoJob::MemoryInfo(tracker); diff --git a/src/crypto/crypto_context.cc b/src/crypto/crypto_context.cc index 757da8503f24a9..3513afc5dfc557 100644 --- a/src/crypto/crypto_context.cc +++ b/src/crypto/crypto_context.cc @@ -1915,8 +1915,13 @@ void SecureContext::SetDHParam(const FunctionCallbackInfo& args) { // true to this function instead of the original string. Any other string // value will be interpreted as custom DH parameters below. if (args[0]->IsTrue()) { +#ifdef SSL_CTX_set_dh_auto CHECK(SSL_CTX_set_dh_auto(sc->ctx_.get(), true)); return; +#else + return THROW_ERR_CRYPTO_UNSUPPORTED_OPERATION( + env, "Automatic DH parameter selection is not supported"); +#endif } DHPointer dh; diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc index e35fda9ad2e8c5..b3b1d44a32a610 100644 --- a/src/crypto/crypto_dh.cc +++ b/src/crypto/crypto_dh.cc @@ -309,15 +309,17 @@ void ComputeSecret(const FunctionCallbackInfo& args) { BignumPointer key(key_buf.data(), key_buf.size()); switch (dh.checkPublicKey(key)) { - case DHPointer::CheckPublicKeyResult::INVALID: - // Fall-through case DHPointer::CheckPublicKeyResult::CHECK_FAILED: return THROW_ERR_CRYPTO_INVALID_KEYTYPE(env, "Unspecified validation error"); +#ifndef OPENSSL_IS_BORINGSSL case DHPointer::CheckPublicKeyResult::TOO_SMALL: return THROW_ERR_CRYPTO_INVALID_KEYLEN(env, "Supplied key is too small"); case DHPointer::CheckPublicKeyResult::TOO_LARGE: return THROW_ERR_CRYPTO_INVALID_KEYLEN(env, "Supplied key is too large"); +#endif + case DHPointer::CheckPublicKeyResult::INVALID: + return THROW_ERR_CRYPTO_INVALID_KEYTYPE(env, "Supplied key is invalid"); case DHPointer::CheckPublicKeyResult::NONE: break; } @@ -505,20 +507,16 @@ Maybe DHBitsTraits::AdditionalConfig( const FunctionCallbackInfo& args, unsigned int offset, DHBitsConfig* params) { - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key - - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; - - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); + auto public_key = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!public_key) [[unlikely]] + return Nothing(); - CHECK(private_key->Data().GetKeyType() == kKeyTypePrivate); - CHECK(public_key->Data().GetKeyType() != kKeyTypeSecret); + auto private_key = KeyObjectData::GetPrivateKeyFromJs(args, &offset, true); + if (!private_key) [[unlikely]] + return Nothing(); - params->public_key = public_key->Data().addRef(); - params->private_key = private_key->Data().addRef(); + params->public_key = std::move(public_key); + params->private_key = std::move(private_key); return JustVoid(); } diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 95e1a68070ed67..cf32c9934701ee 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -39,7 +39,6 @@ using v8::JustVoid; using v8::Local; using v8::LocalVector; using v8::Maybe; -using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::String; @@ -68,7 +67,6 @@ void ECDH::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "ECDHConvertKey", ECDH::ConvertKey); SetMethodNoSideEffect(context, target, "getCurves", ECDH::GetCurves); - ECDHBitsJob::Initialize(env, target); ECKeyPairGenJob::Initialize(env, target); ECKeyExportJob::Initialize(env, target); @@ -87,7 +85,6 @@ void ECDH::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(ECDH::ConvertKey); registry->Register(ECDH::GetCurves); - ECDHBitsJob::RegisterExternalReferences(registry); ECKeyPairGenJob::RegisterExternalReferences(registry); ECKeyExportJob::RegisterExternalReferences(registry); } @@ -393,103 +390,6 @@ void ECDH::ConvertKey(const FunctionCallbackInfo& args) { } } -void ECDHBitsConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("public", public_); - tracker->TrackField("private", private_); -} - -MaybeLocal ECDHBitsTraits::EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe ECDHBitsTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params) { - Environment* env = Environment::GetCurrent(args); - - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key - - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; - - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); - - if (private_key->Data().GetKeyType() != kKeyTypePrivate || - public_key->Data().GetKeyType() != kKeyTypePublic) { - THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); - return Nothing(); - } - - params->private_ = private_key->Data().addRef(); - params->public_ = public_key->Data().addRef(); - - return JustVoid(); -} - -bool ECDHBitsTraits::DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out, - CryptoJobMode mode) { - size_t len = 0; - const auto& m_privkey = params.private_.GetAsymmetricKey(); - const auto& m_pubkey = params.public_.GetAsymmetricKey(); - - switch (m_privkey.id()) { - case EVP_PKEY_X25519: - // Fall through - case EVP_PKEY_X448: { - Mutex::ScopedLock pub_lock(params.public_.mutex()); - EVPKeyCtxPointer ctx = m_privkey.newCtx(); - if (!ctx.initForDerive(m_pubkey)) return false; - - auto data = ctx.derive(); - if (!data) return false; - DCHECK(!data.isSecure()); - - *out = ByteSource::Allocated(data.release()); - break; - } - default: { - const EC_KEY* private_key; - { - Mutex::ScopedLock priv_lock(params.private_.mutex()); - private_key = m_privkey; - } - - Mutex::ScopedLock pub_lock(params.public_.mutex()); - const EC_KEY* public_key = m_pubkey; - - const auto group = ECKeyPointer::GetGroup(private_key); - if (group == nullptr) - return false; - - CHECK(ECKeyPointer::Check(private_key)); - CHECK(ECKeyPointer::Check(public_key)); - const auto pub = ECKeyPointer::GetPublicKey(public_key); - int field_size = EC_GROUP_get_degree(group); - len = (field_size + 7) / 8; - auto buf = DataPointer::Alloc(len); - CHECK_NOT_NULL(pub); - CHECK_NOT_NULL(private_key); - if (ECDH_compute_key( - static_cast(buf.get()), len, pub, private_key, nullptr) <= - 0) { - return false; - } - - *out = ByteSource::Allocated(buf.release()); - } - } - - return true; -} - EVPKeyCtxPointer EcKeyGenTraits::Setup(EcKeyPairGenConfig* params) { EVPKeyCtxPointer key_ctx; switch (params->params.curve_nid) { @@ -538,7 +438,6 @@ Maybe EcKeyGenTraits::AdditionalConfig( EcKeyPairGenConfig* params) { Environment* env = Environment::GetCurrent(args); CHECK(args[*offset]->IsString()); // curve name - CHECK(args[*offset + 1]->IsInt32()); // param encoding Utf8Value curve_name(env->isolate(), args[*offset]); params->params.curve_nid = Ec::GetCurveIdFromName(*curve_name); @@ -547,11 +446,17 @@ Maybe EcKeyGenTraits::AdditionalConfig( return Nothing(); } - params->params.param_encoding = args[*offset + 1].As()->Value(); - if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE && - params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) { - THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified"); - return Nothing(); + // param encoding + if (args[*offset + 1]->IsNullOrUndefined()) { + params->params.param_encoding = OPENSSL_EC_NAMED_CURVE; + } else { + CHECK(args[*offset + 1]->IsInt32()); + params->params.param_encoding = args[*offset + 1].As()->Value(); + if (params->params.param_encoding != OPENSSL_EC_NAMED_CURVE && + params->params.param_encoding != OPENSSL_EC_EXPLICIT_CURVE) { + THROW_ERR_OUT_OF_RANGE(env, "Invalid param_encoding specified"); + return Nothing(); + } } *offset += 2; @@ -716,10 +621,10 @@ bool ExportJWKEcKey(Environment* env, return false; } - if (target->Set( - env->context(), - env->jwk_kty_string(), - env->jwk_ec_string()).IsNothing()) { + if (!target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_ec_string()) + .FromMaybe(false)) { return false; } @@ -759,10 +664,9 @@ bool ExportJWKEcKey(Environment* env, return false; } } - if (target->Set( - env->context(), - env->jwk_crv_string(), - crv_name).IsNothing()) { + if (!target + ->DefineOwnProperty(env->context(), env->jwk_crv_string(), crv_name) + .FromMaybe(false)) { return false; } @@ -805,29 +709,101 @@ bool ExportJWKEdKey(Environment* env, const ncrypto::Buffer out = data; return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); }; return !( - target - ->Set(env->context(), - env->jwk_crv_string(), - OneByteString(env->isolate(), curve)) - .IsNothing() || + !target + ->DefineOwnProperty(env->context(), + env->jwk_crv_string(), + OneByteString(env->isolate(), curve)) + .FromMaybe(false) || (key.GetKeyType() == kKeyTypePrivate && !trySetKey(env, pkey.rawPrivateKey(), target, env->jwk_d_string())) || !trySetKey(env, pkey.rawPublicKey(), target, env->jwk_x_string()) || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_okp_string()) - .IsNothing()); + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_okp_string()) + .FromMaybe(false)); } +KeyObjectData ImportJWKEdKey(Environment* env, Local jwk) { + Local crv_value; + Local x_value; + Local d_value; + + if (!jwk->Get(env->context(), env->jwk_crv_string()).ToLocal(&crv_value) || + !jwk->Get(env->context(), env->jwk_x_string()).ToLocal(&x_value) || + !jwk->Get(env->context(), env->jwk_d_string()).ToLocal(&d_value)) { + return {}; + } + + if (!crv_value->IsString() || !x_value->IsString() || + (!d_value->IsUndefined() && !d_value->IsString())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK OKP key"); + return {}; + } + + Utf8Value crv(env->isolate(), crv_value.As()); + + static constexpr struct { + const char* name; + int nid; + } kCurveToNid[] = { + {"Ed25519", EVP_PKEY_ED25519}, + {"Ed448", EVP_PKEY_ED448}, + {"X25519", EVP_PKEY_X25519}, + {"X448", EVP_PKEY_X448}, + }; + + int id = NID_undef; + for (const auto& entry : kCurveToNid) { + if (strcmp(*crv, entry.name) == 0) { + id = entry.nid; + break; + } + } -KeyObjectData ImportJWKEcKey(Environment* env, - Local jwk, - const FunctionCallbackInfo& args, - unsigned int offset) { - CHECK(args[offset]->IsString()); // curve name - Utf8Value curve(env->isolate(), args[offset].As()); + if (id == NID_undef) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK OKP key"); + return {}; + } + + KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + + ByteSource raw; + if (type == kKeyTypePrivate) { + raw = ByteSource::FromEncodedString(env, d_value.As()); + } else { + raw = ByteSource::FromEncodedString(env, x_value.As()); + } + + typedef EVPKeyPointer (*new_key_fn)( + int, const ncrypto::Buffer&); + new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate + : EVPKeyPointer::NewRawPublic; + + auto pkey = fn(id, + ncrypto::Buffer{ + .data = raw.data(), + .len = raw.size(), + }); + if (!pkey) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK OKP key"); + return {}; + } + + return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); +} +KeyObjectData ImportJWKEcKey(Environment* env, Local jwk) { + Local crv_value; + if (!jwk->Get(env->context(), env->jwk_crv_string()).ToLocal(&crv_value) || + !crv_value->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); + return {}; + } + Utf8Value curve(env->isolate(), crv_value.As()); int nid = Ec::GetCurveIdFromName(*curve); if (nid == NID_undef) { // Unknown curve THROW_ERR_CRYPTO_INVALID_CURVE(env); @@ -862,6 +838,8 @@ KeyObjectData ImportJWKEcKey(Environment* env, ByteSource x = ByteSource::FromEncodedString(env, x_value.As()); ByteSource y = ByteSource::FromEncodedString(env, y_value.As()); + // setPublicKeyRaw validates the point is on the curve. For h=1 curves + // (P-256/P-384/P-521), this skips EC_KEY_check_key for efficiency. if (!ec.setPublicKeyRaw(x.ToBN(), y.ToBN())) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); return {}; diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 101d07e54e8e57..d9f8dcfc814909 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -55,40 +55,6 @@ class ECDH final : public BaseObject { const EC_GROUP* group_; }; -struct ECDHBitsConfig final : public MemoryRetainer { - int id_; - KeyObjectData private_; - KeyObjectData public_; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(ECDHBitsConfig) - SET_SELF_SIZE(ECDHBitsConfig) -}; - -struct ECDHBitsTraits final { - using AdditionalParameters = ECDHBitsConfig; - static constexpr const char* JobName = "ECDHBitsJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_DERIVEBITSREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params); - - static bool DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out_, - CryptoJobMode mode); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out); -}; - -using ECDHBitsJob = DeriveBitsJob; - struct EcKeyPairParams final : public MemoryRetainer { int curve_nid; int param_encoding; @@ -148,10 +114,9 @@ bool ExportJWKEdKey(Environment* env, const KeyObjectData& key, v8::Local target); -KeyObjectData ImportJWKEcKey(Environment* env, - v8::Local jwk, - const v8::FunctionCallbackInfo& args, - unsigned int offset); +KeyObjectData ImportJWKEdKey(Environment* env, v8::Local jwk); + +KeyObjectData ImportJWKEcKey(Environment* env, v8::Local jwk); bool GetEcKeyDetail(Environment* env, const KeyObjectData& key, diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 33cde71b105c7c..58c4a45986bf0f 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,6 +7,10 @@ #include "threadpoolwork-inl.h" #include "v8.h" +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +#include +#endif + #include namespace node { @@ -41,6 +45,24 @@ void Hash::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackFieldWithSize("md", digest_ ? md_len_ : 0); } +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK +struct BoringSSLDigest { + const EVP_MD* (*get)(); + const char* name; +}; + +constexpr BoringSSLDigest kBoringSSLDigests[] = { + {EVP_md4, "md4"}, + {EVP_md5, "md5"}, + {EVP_sha1, "sha1"}, + {EVP_sha224, "sha224"}, + {EVP_sha256, "sha256"}, + {EVP_sha384, "sha384"}, + {EVP_sha512, "sha512"}, + {EVP_sha512_256, "sha512-256"}, +}; +#endif + #if OPENSSL_VERSION_MAJOR >= 3 void PushAliases(const char* name, void* data) { static_cast*>(data)->push_back(name); @@ -122,7 +144,12 @@ void SaveSupportedHashAlgorithms(const EVP_MD* md, const std::vector& GetSupportedHashAlgorithms(Environment* env) { if (env->supported_hash_algorithms.empty()) { MarkPopErrorOnReturn mark_pop_error_on_return; -#if OPENSSL_VERSION_MAJOR >= 3 +#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK + for (const auto& digest : kBoringSSLDigests) { + static_cast(digest.get); + env->supported_hash_algorithms.emplace_back(digest.name); + } +#elif OPENSSL_VERSION_MAJOR >= 3 // Since we'll fetch the EVP_MD*, cache them along the way to speed up // later lookups instead of throwing them away immediately. EVP_MD_do_all_sorted(SaveSupportedHashAlgorithmsAndCacheMD, env); @@ -479,8 +506,7 @@ HashConfig& HashConfig::operator=(HashConfig&& other) noexcept { void HashConfig::MemoryInfo(MemoryTracker* tracker) const { // If the Job is sync, then the HashConfig does not own the data. - if (mode == kCryptoJobAsync) - tracker->TrackFieldWithSize("in", in.size()); + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("in", in.size()); } MaybeLocal HashTraits::EncodeOutput(Environment* env, @@ -511,9 +537,7 @@ Maybe HashTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->in = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); unsigned int expected = EVP_MD_size(params->digest); params->length = expected; diff --git a/src/crypto/crypto_hkdf.cc b/src/crypto/crypto_hkdf.cc index 2f135cb1c2f8ea..52e81f814083d0 100644 --- a/src/crypto/crypto_hkdf.cc +++ b/src/crypto/crypto_hkdf.cc @@ -24,6 +24,7 @@ HKDFConfig::HKDFConfig(HKDFConfig&& other) noexcept length(other.length), digest(other.digest), key(std::move(other.key)), + key_data(std::move(other.key_data)), salt(std::move(other.salt)), info(std::move(other.info)) {} @@ -49,7 +50,8 @@ Maybe HKDFTraits::AdditionalConfig( params->mode = mode; CHECK(args[offset]->IsString()); // Hash - CHECK(args[offset + 1]->IsObject()); // Key + CHECK(KeyObjectHandle::HasInstance(env, args[offset + 1]) || + IsAnyBufferSource(args[offset + 1])); // Key CHECK(IsAnyBufferSource(args[offset + 2])); // Salt CHECK(IsAnyBufferSource(args[offset + 3])); // Info CHECK(args[offset + 4]->IsUint32()); // Length @@ -61,9 +63,19 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); - params->key = key->Data().addRef(); + if (KeyObjectHandle::HasInstance(env, args[offset + 1])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset + 1], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents key_data(args[offset + 1]); + if (!key_data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "key is too big"); + return Nothing(); + } + params->key_data = + IsCryptoJobAsync(mode) ? key_data.ToCopy() : key_data.ToByteSource(); + } ArrayBufferOrViewContents salt(args[offset + 2]); ArrayBufferOrViewContents info(args[offset + 3]); @@ -77,13 +89,9 @@ Maybe HKDFTraits::AdditionalConfig( return Nothing(); } - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); - params->info = mode == kCryptoJobAsync - ? info.ToCopy() - : info.ToByteSource(); + params->info = IsCryptoJobAsync(mode) ? info.ToCopy() : info.ToByteSource(); params->length = args[offset + 4].As()->Value(); // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as the @@ -101,12 +109,16 @@ bool HKDFTraits::DeriveBits(Environment* env, const HKDFConfig& params, ByteSource* out, CryptoJobMode mode) { + const ncrypto::Buffer key_data{ + .data = params.key ? reinterpret_cast( + params.key.GetSymmetricKey()) + : params.key_data.data(), + .len = params.key ? params.key.GetSymmetricKeySize() + : params.key_data.size(), + }; + auto dp = ncrypto::hkdf(params.digest, - ncrypto::Buffer{ - .data = reinterpret_cast( - params.key.GetSymmetricKey()), - .len = params.key.GetSymmetricKeySize(), - }, + key_data, ncrypto::Buffer{ .data = params.info.data(), .len = params.info.size(), @@ -124,9 +136,10 @@ bool HKDFTraits::DeriveBits(Environment* env, } void HKDFConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("key", key); + if (key) tracker->TrackField("key", key); // If the job is sync, then the HKDFConfig does not own the data - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("key", key_data.size()); tracker->TrackFieldWithSize("salt", salt.size()); tracker->TrackFieldWithSize("info", info.size()); } diff --git a/src/crypto/crypto_hkdf.h b/src/crypto/crypto_hkdf.h index 29bdd5b5d77d10..f87ae4a8f91bb2 100644 --- a/src/crypto/crypto_hkdf.h +++ b/src/crypto/crypto_hkdf.h @@ -16,6 +16,7 @@ struct HKDFConfig final : public MemoryRetainer { size_t length; ncrypto::Digest digest; KeyObjectData key; + ByteSource key_data; ByteSource salt; ByteSource info; diff --git a/src/crypto/crypto_hmac.cc b/src/crypto/crypto_hmac.cc index 88a512d7550200..c0147f9d5712be 100644 --- a/src/crypto/crypto_hmac.cc +++ b/src/crypto/crypto_hmac.cc @@ -172,7 +172,7 @@ HmacConfig& HmacConfig::operator=(HmacConfig&& other) noexcept { void HmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the HmacConfig does not own the data - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); } @@ -210,9 +210,7 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 4]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 4]); @@ -220,9 +218,8 @@ Maybe HmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); } - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); diff --git a/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc index d6227bb66c6dc1..c14866e6af56cb 100644 --- a/src/crypto/crypto_kem.cc +++ b/src/crypto/crypto_kem.cc @@ -1,6 +1,6 @@ #include "crypto/crypto_kem.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #include "async_wrap-inl.h" #include "base_object-inl.h" @@ -16,6 +16,7 @@ namespace node { using ncrypto::EVPKeyPointer; using v8::Array; +using v8::ArrayBufferView; using v8::FunctionCallbackInfo; using v8::Local; using v8::Maybe; @@ -41,7 +42,7 @@ KEMConfiguration& KEMConfiguration::operator=( void KEMConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("ciphertext", ciphertext.size()); } } @@ -174,6 +175,23 @@ MaybeLocal KEMEncapsulateTraits::EncodeOutput( return MaybeLocal(); } + if (params.job_mode == kCryptoJobWebCrypto) { + Local result = Object::New(env->isolate()); + if (!result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "sharedKey"), + shared_key_obj.As()->Buffer()) + .FromMaybe(false) || + !result + ->DefineOwnProperty(env->context(), + OneByteString(env->isolate(), "ciphertext"), + ciphertext_obj.As()->Buffer()) + .FromMaybe(false)) { + return MaybeLocal(); + } + return result; + } + // Return an array [sharedKey, ciphertext]. Local result = Array::New(env->isolate(), 2); if (result->Set(env->context(), 0, shared_key_obj).IsNothing() || @@ -210,7 +228,7 @@ Maybe KEMDecapsulateTraits::AdditionalConfig( } params->ciphertext = - mode == kCryptoJobAsync ? ciphertext.ToCopy() : ciphertext.ToByteSource(); + IsCryptoJobAsync(mode) ? ciphertext.ToCopy() : ciphertext.ToByteSource(); return v8::JustVoid(); } diff --git a/src/crypto/crypto_kem.h b/src/crypto/crypto_kem.h index 02cef53c27c8c8..d4eb56ea4027e8 100644 --- a/src/crypto/crypto_kem.h +++ b/src/crypto/crypto_kem.h @@ -10,7 +10,7 @@ #include "memory_tracker.h" #include "node_external_reference.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM namespace node { namespace crypto { diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index e43d8cb0475ff2..1702dfabb4af2a 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -22,6 +22,20 @@ enum class KeyGenJobStatus { FAILED }; +struct WebCryptoKeyGenConfig final { + v8::Global algorithm; + uint32_t usages_mask = 0; + uint32_t public_usages_mask = 0; + uint32_t private_usages_mask = 0; + bool extractable = false; + + WebCryptoKeyGenConfig() = default; + WebCryptoKeyGenConfig(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig& operator=(WebCryptoKeyGenConfig&&) = default; + WebCryptoKeyGenConfig(const WebCryptoKeyGenConfig&) = delete; + WebCryptoKeyGenConfig& operator=(const WebCryptoKeyGenConfig&) = delete; +}; + // A Base CryptoJob for generating secret keys or key pairs. // The KeyGenTraits is largely responsible for the details of // the implementation, while KeyGenJob handles the common @@ -48,7 +62,29 @@ class KeyGenJob final : public CryptoJob { return; } - new KeyGenJob(env, args.This(), mode, std::move(params)); + WebCryptoKeyGenConfig config; + if (mode == kCryptoJobWebCrypto) { + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsUint32()); + CHECK(args[offset + 3]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.public_usages_mask = args[offset + 1].As()->Value(); + config.private_usages_mask = args[offset + 2].As()->Value(); + config.extractable = args[offset + 3]->IsTrue(); + } else { + CHECK(args[offset]->IsObject()); + CHECK(args[offset + 1]->IsUint32()); + CHECK(args[offset + 2]->IsBoolean()); + config.algorithm.Reset(env->isolate(), args[offset]); + config.usages_mask = args[offset + 1].As()->Value(); + config.extractable = args[offset + 2]->IsTrue(); + } + } + + new KeyGenJob( + env, args.This(), mode, std::move(params), std::move(config)); } static void Initialize( @@ -61,17 +97,14 @@ class KeyGenJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - KeyGenJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + KeyGenJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params, + WebCryptoKeyGenConfig&& config) : CryptoJob( - env, - object, - KeyGenTraits::Provider, - mode, - std::move(params)) {} + env, object, KeyGenTraits::Provider, mode, std::move(params)), + webcrypto_config_(std::move(config)) {} void DoThreadPoolWork() override { AdditionalParams* params = CryptoJob::params(); @@ -98,7 +131,11 @@ class KeyGenJob final : public CryptoJob { if (status_ == KeyGenJobStatus::OK) { v8::TryCatch try_catch(env->isolate()); - if (KeyGenTraits::EncodeKey(env, params).ToLocal(result)) { + v8::MaybeLocal encoded = + CryptoJob::mode() == kCryptoJobWebCrypto + ? EncodeWebCryptoKey(env, params) + : KeyGenTraits::EncodeKey(env, params); + if (encoded.ToLocal(result)) { *err = Undefined(env->isolate()); } else { CHECK(try_catch.HasCaught()); @@ -122,6 +159,53 @@ class KeyGenJob final : public CryptoJob { SET_SELF_SIZE(KeyGenJob) private: + v8::MaybeLocal EncodeWebCryptoKey(Environment* env, + AdditionalParams* params) { + v8::Isolate* isolate = env->isolate(); + v8::Local algorithm = + v8::Local::New(isolate, webcrypto_config_.algorithm); + + if constexpr (KeyGenTraits::kWebCryptoKeyPair) { + v8::Local public_key; + v8::Local private_key; + if (!NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePublic), + algorithm, + webcrypto_config_.public_usages_mask, + true) + .ToLocal(&public_key) || + !NativeCryptoKey::Create(env, + params->key.addRefWithType(kKeyTypePrivate), + algorithm, + webcrypto_config_.private_usages_mask, + webcrypto_config_.extractable) + .ToLocal(&private_key)) { + return {}; + } + + v8::Local ret = v8::Object::New(isolate); + if (!ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "publicKey"), + public_key) + .FromMaybe(false) || + !ret->DefineOwnProperty(env->context(), + OneByteString(isolate, "privateKey"), + private_key) + .FromMaybe(false)) { + return {}; + } + return ret; + } else { + auto data = KeyObjectData::CreateSecret(std::move(params->out)); + return NativeCryptoKey::Create(env, + data, + algorithm, + webcrypto_config_.usages_mask, + webcrypto_config_.extractable); + } + } + + WebCryptoKeyGenConfig webcrypto_config_; KeyGenJobStatus status_ = KeyGenJobStatus::FAILED; }; @@ -130,6 +214,7 @@ template struct KeyPairGenTraits final { using AdditionalParameters = typename KeyPairAlgorithmTraits::AdditionalParameters; + static constexpr bool kWebCryptoKeyPair = true; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYPAIRGENREQUEST; @@ -146,8 +231,13 @@ struct KeyPairGenTraits final { // process input parameters. This allows each job to have a variable // number of input parameters specific to each job type. if (KeyPairAlgorithmTraits::AdditionalConfig(mode, args, offset, params) - .IsNothing() || - !KeyObjectData::GetPublicKeyEncodingFromJs( + .IsNothing()) { + return v8::Nothing(); + } + + if (mode == kCryptoJobWebCrypto) return v8::JustVoid(); + + if (!KeyObjectData::GetPublicKeyEncodingFromJs( args, offset, kKeyContextGenerate) .To(¶ms->public_key_encoding) || !KeyObjectData::GetPrivateKeyEncodingFromJs( @@ -204,6 +294,7 @@ struct SecretKeyGenConfig final : public MemoryRetainer { struct SecretKeyGenTraits final { using AdditionalParameters = SecretKeyGenConfig; + static constexpr bool kWebCryptoKeyPair = false; static const AsyncWrap::ProviderType Provider = AsyncWrap::PROVIDER_KEYGENREQUEST; static constexpr const char* JobName = "SecretKeyGenJob"; @@ -287,4 +378,3 @@ using SecretKeyGenJob = KeyGenJob; #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS #endif // SRC_CRYPTO_CRYPTO_KEYGEN_H_ - diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index c05872d73f6fc2..81451d88f7e4b7 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -5,7 +5,7 @@ #include "crypto/crypto_dh.h" #include "crypto/crypto_dsa.h" #include "crypto/crypto_ec.h" -#include "crypto/crypto_ml_dsa.h" +#include "crypto/crypto_pqc.h" #include "crypto/crypto_rsa.h" #include "crypto/crypto_util.h" #include "env-inl.h" @@ -28,6 +28,7 @@ using ncrypto::EVPKeyPointer; using ncrypto::MarkPopErrorOnReturn; using ncrypto::PKCS8Pointer; using v8::Array; +using v8::Boolean; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -156,9 +157,11 @@ bool ExportJWKSecretKey(Environment* env, BASE64URL) .ToLocal(&raw) && target - ->Set(env->context(), env->jwk_kty_string(), env->jwk_oct_string()) - .IsJust() && - target->Set(env->context(), env->jwk_k_string(), raw).IsJust(); + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_oct_string()) + .FromMaybe(false) && + target->DefineOwnProperty(env->context(), env->jwk_k_string(), raw) + .FromMaybe(false); } KeyObjectData ImportJWKSecretKey(Environment* env, Local jwk) { @@ -178,7 +181,11 @@ bool ExportJWKAsymmetricKey(Environment* env, const KeyObjectData& key, Local target, bool handleRsaPss) { - switch (key.GetAsymmetricKey().id()) { + const int id = key.GetAsymmetricKey().id(); +#if OPENSSL_WITH_PQC + if (IsPqcKeyId(id)) return ExportJwkPqcKey(env, key, target); +#endif + switch (id) { case EVP_PKEY_RSA_PSS: { if (handleRsaPss) return ExportJWKRsaKey(env, key, target); break; @@ -188,42 +195,15 @@ bool ExportJWKAsymmetricKey(Environment* env, case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target); case EVP_PKEY_ED25519: - // Fall through case EVP_PKEY_ED448: - // Fall through case EVP_PKEY_X25519: - // Fall through case EVP_PKEY_X448: return ExportJWKEdKey(env, key, target); -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - // Fall through - case EVP_PKEY_ML_DSA_65: - // Fall through - case EVP_PKEY_ML_DSA_87: - return ExportJwkMlDsaKey(env, key, target); -#endif } THROW_ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE(env); return false; } -KeyObjectData ImportJWKAsymmetricKey(Environment* env, - Local jwk, - std::string_view kty, - const FunctionCallbackInfo& args, - unsigned int offset) { - if (kty == "RSA") { - return ImportJWKRsaKey(env, jwk, args, offset); - } else if (kty == "EC") { - return ImportJWKEcKey(env, jwk, args, offset); - } - - THROW_ERR_CRYPTO_INVALID_JWK( - env, "%s is not a supported JWK key type", kty.data()); - return {}; -} - bool GetSecretKeyDetail(Environment* env, const KeyObjectData& key, Local target) { @@ -297,33 +277,15 @@ int GetNidFromName(const char* name) { {"Ed448", EVP_PKEY_ED448}, {"X25519", EVP_PKEY_X25519}, {"X448", EVP_PKEY_X448}, -#if OPENSSL_WITH_PQC - {"ML-DSA-44", EVP_PKEY_ML_DSA_44}, - {"ML-DSA-65", EVP_PKEY_ML_DSA_65}, - {"ML-DSA-87", EVP_PKEY_ML_DSA_87}, - {"ML-KEM-512", EVP_PKEY_ML_KEM_512}, - {"ML-KEM-768", EVP_PKEY_ML_KEM_768}, - {"ML-KEM-1024", EVP_PKEY_ML_KEM_1024}, - {"SLH-DSA-SHA2-128f", EVP_PKEY_SLH_DSA_SHA2_128F}, - {"SLH-DSA-SHA2-128s", EVP_PKEY_SLH_DSA_SHA2_128S}, - {"SLH-DSA-SHA2-192f", EVP_PKEY_SLH_DSA_SHA2_192F}, - {"SLH-DSA-SHA2-192s", EVP_PKEY_SLH_DSA_SHA2_192S}, - {"SLH-DSA-SHA2-256f", EVP_PKEY_SLH_DSA_SHA2_256F}, - {"SLH-DSA-SHA2-256s", EVP_PKEY_SLH_DSA_SHA2_256S}, - {"SLH-DSA-SHAKE-128f", EVP_PKEY_SLH_DSA_SHAKE_128F}, - {"SLH-DSA-SHAKE-128s", EVP_PKEY_SLH_DSA_SHAKE_128S}, - {"SLH-DSA-SHAKE-192f", EVP_PKEY_SLH_DSA_SHAKE_192F}, - {"SLH-DSA-SHAKE-192s", EVP_PKEY_SLH_DSA_SHAKE_192S}, - {"SLH-DSA-SHAKE-256f", EVP_PKEY_SLH_DSA_SHAKE_256F}, - {"SLH-DSA-SHAKE-256s", EVP_PKEY_SLH_DSA_SHAKE_256S}, -#endif }; for (const auto& entry : kNameToNid) { - if (StringEqualNoCase(name, entry.name)) { - return entry.nid; - } + if (StringEqualNoCase(name, entry.name)) return entry.nid; } +#if OPENSSL_WITH_PQC + return GetPqcNidFromName(name); +#else return NID_undef; +#endif } } // namespace @@ -352,35 +314,15 @@ bool KeyObjectData::ToEncodedPublicKey( const auto point = ECKeyPointer::GetPublicKey(ec_key); return ECPointToBuffer(env, group, point, form).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPublicKey(); if (!raw_data) { @@ -427,29 +369,15 @@ bool KeyObjectData::ToEncodedPrivateKey( } return Buffer::Copy(env, buf.get(), buf.size()).ToLocal(out); } - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif - break; - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!is_raw_supported) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } auto raw_data = pkey.rawPrivateKey(); if (!raw_data) { @@ -459,23 +387,13 @@ bool KeyObjectData::ToEncodedPrivateKey( return Buffer::Copy(env, raw_data.get(), raw_data.size()) .ToLocal(out); } else if (config.format == EVPKeyPointer::PKFormatType::RAW_SEED) { +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); - switch (pkey.id()) { -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - break; -#endif - default: - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; + if (!IsPqcSeedKeyId(pkey.id())) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return false; } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -558,12 +476,257 @@ KeyObjectData::GetPublicKeyEncodingFromJs( return GetKeyFormatAndTypeFromJs(args, offset, context); } +// Shared helper for importing raw asymmetric keys. Called from +// ImportRawKeyFromArgs. +static KeyObjectData ImportRawKey(Environment* env, + const unsigned char* key_data, + size_t key_data_len, + EVPKeyPointer::PKFormatType format, + Local key_type, + const char* key_type_name, + const char* named_curve, + KeyType target_type) { + auto throw_invalid = [&]() { + if (!env->isolate()->HasPendingException()) { + THROW_ERR_INVALID_ARG_VALUE(env, "Invalid key data"); + } + }; + + // EC keys + if (key_type->StringEquals(env->crypto_ec_string())) { + int curve_nid = ncrypto::Ec::GetCurveIdFromName(named_curve); + if (curve_nid == NID_undef) { + THROW_ERR_CRYPTO_INVALID_CURVE(env); + return {}; + } + auto eckey = ECKeyPointer::NewByCurveName(curve_nid); + if (!eckey) { + throw_invalid(); + return {}; + } + if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC) { + const auto group = eckey.getGroup(); + auto pub = ECPointPointer::New(group); + if (!pub) { + throw_invalid(); + return {}; + } + ncrypto::Buffer buffer{ + .data = key_data, + .len = key_data_len, + }; + if (!pub.setFromBuffer(buffer, group) || !eckey.setPublicKey(pub)) { + throw_invalid(); + return {}; + } + } else { + const auto group = eckey.getGroup(); + auto order = BignumPointer::New(); + CHECK(order); + CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); + if (key_data_len != order.byteLength()) { + throw_invalid(); + return {}; + } + BignumPointer priv_bn(key_data, key_data_len); + if (!priv_bn || !eckey.setPrivateKey(priv_bn)) { + throw_invalid(); + return {}; + } + auto pub_point = ECPointPointer::New(group); + if (!pub_point || !pub_point.mul(group, priv_bn.get()) || + !eckey.setPublicKey(pub_point)) { + throw_invalid(); + return {}; + } + } + auto pkey = EVPKeyPointer::New(); + if (!pkey.assign(eckey)) { + throw_invalid(); + return {}; + } + eckey.release(); + return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey)); + } + + int id = GetNidFromName(key_type_name); + + typedef EVPKeyPointer (*new_key_fn)( + int, const ncrypto::Buffer&); + new_key_fn fn = nullptr; + switch (id) { + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + fn = target_type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate + : EVPKeyPointer::NewRawPublic; + break; + default: +#if OPENSSL_WITH_PQC + if (IsPqcKeyId(id)) { + if (target_type == kKeyTypePrivate) { + fn = IsPqcSeedKeyId(id) ? EVPKeyPointer::NewRawSeed + : EVPKeyPointer::NewRawPrivate; + } else { + fn = EVPKeyPointer::NewRawPublic; + } + } +#endif + break; + } + + if (fn != nullptr) { + auto pkey = fn(id, + ncrypto::Buffer{ + .data = key_data, + .len = key_data_len, + }); + if (!pkey) { + throw_invalid(); + return {}; + } + return KeyObjectData::CreateAsymmetric(target_type, std::move(pkey)); + } + + if (key_type->StringEquals(env->crypto_rsa_string()) || + key_type->StringEquals(env->crypto_rsa_pss_string()) || + key_type->StringEquals(env->crypto_dsa_string()) || + key_type->StringEquals(env->crypto_dh_string())) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return {}; + } + +#if !OPENSSL_WITH_PQC + if (key_type->StringEquals(env->crypto_ml_dsa_44_string()) || + key_type->StringEquals(env->crypto_ml_dsa_65_string()) || + key_type->StringEquals(env->crypto_ml_dsa_87_string()) || + key_type->StringEquals(env->crypto_ml_kem_512_string()) || + key_type->StringEquals(env->crypto_ml_kem_768_string()) || + key_type->StringEquals(env->crypto_ml_kem_1024_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_128f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_128s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_192f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_192s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_256f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_sha2_256s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_128f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_128s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_192f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_192s_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_256f_string()) || + key_type->StringEquals(env->crypto_slh_dsa_shake_256s_string())) { + THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); + return {}; + } +#endif + + THROW_ERR_INVALID_ARG_VALUE( + env, "Invalid asymmetricKeyType: %s", key_type_name); + return {}; +} + +// Shared helper for importing a JWK asymmetric key. Extracts kty from the +// JWK object and dispatches to the appropriate importer. +static KeyObjectData ImportJWKFromArgs(Environment* env, Local jwk) { + Local kty; + if (!jwk->Get(env->context(), env->jwk_kty_string()).ToLocal(&kty) || + !kty->IsString()) { + THROW_ERR_CRYPTO_INVALID_JWK(env); + return {}; + } + Utf8Value kty_string(env->isolate(), kty); + if (*kty_string == std::string_view("RSA")) { + return ImportJWKRsaKey(env, jwk); + } else if (*kty_string == std::string_view("EC")) { + return ImportJWKEcKey(env, jwk); + } else if (*kty_string == std::string_view("OKP")) { + return ImportJWKEdKey(env, jwk); + } else if (*kty_string == std::string_view("AKP")) { +#if OPENSSL_WITH_PQC + return ImportJWKPqcKey(env, jwk); +#else + THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); + return {}; +#endif + } + + THROW_ERR_CRYPTO_INVALID_JWK( + env, "%s is not a supported JWK key type", *kty_string); + return {}; +} + +// Shared helper for importing raw asymmetric keys from positional args. +// args layout: [... offset+0: buffer, offset+1: formatInt, +// offset+2: asymmetricKeyType, offset+3: passphrase, +// offset+4: namedCurve] +static KeyObjectData ImportRawKeyFromArgs( + const FunctionCallbackInfo& args, unsigned int offset) { + Environment* env = Environment::GetCurrent(args); + + auto format = static_cast( + args[offset + 1].As()->Value()); + KeyType type = (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC) + ? kKeyTypePublic + : kKeyTypePrivate; + + ArrayBufferOrViewContents key_data(args[offset]); + if (!key_data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "keyData is too big"); + return {}; + } + + CHECK(args[offset + 2]->IsString()); + Local key_type = args[offset + 2].As(); + Utf8Value key_type_name(env->isolate(), key_type); + + DCHECK_IMPLIES(key_type->StringEquals(env->crypto_ec_string()), + args[offset + 4]->IsString()); + Utf8Value curve(env->isolate(), + args[offset + 4]->IsString() ? args[offset + 4].As() + : String::Empty(env->isolate())); + + return ImportRawKey(env, + key_data.data(), + key_data.size(), + format, + key_type, + *key_type_name, + *curve, + type); +} + KeyObjectData KeyObjectData::GetPrivateKeyFromJs( const v8::FunctionCallbackInfo& args, unsigned int* offset, bool allow_key_object) { + Environment* env = Environment::GetCurrent(args); + + // JWK format: data is a JS Object (not buffer), format int is JWK. + if (args[*offset]->IsObject() && !IsAnyBufferSource(args[*offset]) && + args[*offset + 1]->IsInt32()) { + auto format = static_cast( + args[*offset + 1].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::JWK) { + auto data = ImportJWKFromArgs(env, args[*offset].As()); + *offset += 5; + return data; + } + } + if (args[*offset]->IsString() || IsAnyBufferSource(args[*offset])) { - Environment* env = Environment::GetCurrent(args); + // Raw format: buffer + raw format int. + if (args[*offset + 1]->IsInt32()) { + auto format = static_cast( + args[*offset + 1].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::RAW_PRIVATE || + format == EVPKeyPointer::PKFormatType::RAW_SEED) { + auto data = ImportRawKeyFromArgs(args, *offset); + *offset += 5; + return data; + } + } + auto key = ByteSource::FromStringOrBuffer(env, args[(*offset)++]); EVPKeyPointer::PrivateKeyEncodingConfig config; @@ -572,6 +735,9 @@ KeyObjectData KeyObjectData::GetPrivateKeyFromJs( return {}; } + // Skip the namedCurve argument (only used by raw format imports). + (*offset)++; + return TryParsePrivateKey( env, config, @@ -585,14 +751,40 @@ KeyObjectData KeyObjectData::GetPrivateKeyFromJs( KeyObjectHandle* key; ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), KeyObjectData()); CHECK_EQ(key->Data().GetKeyType(), kKeyTypePrivate); - (*offset) += 4; + (*offset) += 5; return key->Data().addRef(); } KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs( const FunctionCallbackInfo& args, unsigned int* offset) { - if (IsAnyBufferSource(args[*offset])) { - Environment* env = Environment::GetCurrent(args); + Environment* env = Environment::GetCurrent(args); + + // JWK format: data is a JS Object (not buffer), format int is JWK. + if (args[*offset]->IsObject() && !IsAnyBufferSource(args[*offset]) && + args[*offset + 1]->IsInt32()) { + auto format = static_cast( + args[*offset + 1].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::JWK) { + auto data = ImportJWKFromArgs(env, args[*offset].As()); + *offset += 5; + return data; + } + } + + if (args[*offset]->IsString() || IsAnyBufferSource(args[*offset])) { + // Raw format: buffer + raw format int. + if (args[*offset + 1]->IsInt32()) { + auto format = static_cast( + args[*offset + 1].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC || + format == EVPKeyPointer::PKFormatType::RAW_PRIVATE || + format == EVPKeyPointer::PKFormatType::RAW_SEED) { + auto data = ImportRawKeyFromArgs(args, *offset); + *offset += 5; + return data; + } + } + ArrayBufferOrViewContents data(args[(*offset)++]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "keyData is too big"); @@ -606,6 +798,9 @@ KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs( return {}; } + // Skip the namedCurve argument (only used by raw format imports). + (*offset)++; + ncrypto::Buffer buffer = { .data = reinterpret_cast(data.data()), .len = data.size(), @@ -664,7 +859,7 @@ KeyObjectData KeyObjectData::GetPublicOrPrivateKeyFromJs( BaseObject::Unwrap(args[*offset].As()); CHECK_NOT_NULL(key); CHECK_NE(key->Data().GetKeyType(), kKeyTypeSecret); - (*offset) += 4; + (*offset) += 5; return key->Data().addRef(); } @@ -786,18 +981,13 @@ Local KeyObjectHandle::Initialize(Environment* env) { isolate, templ, "checkEcKeyData", CheckEcKeyData); SetProtoMethod(isolate, templ, "export", Export); SetProtoMethod(isolate, templ, "exportJwk", ExportJWK); - SetProtoMethod(isolate, templ, "initECRaw", InitECRaw); - SetProtoMethod(isolate, templ, "initEDRaw", InitEDRaw); - SetProtoMethod(isolate, templ, "initPqcRaw", InitPqcRaw); - SetProtoMethod(isolate, templ, "initECPrivateRaw", InitECPrivateRaw); SetProtoMethodNoSideEffect(isolate, templ, "rawPublicKey", RawPublicKey); SetProtoMethodNoSideEffect(isolate, templ, "rawPrivateKey", RawPrivateKey); + SetProtoMethodNoSideEffect(isolate, templ, "rawSeed", RawSeed); SetProtoMethodNoSideEffect( isolate, templ, "exportECPublicRaw", ExportECPublicRaw); SetProtoMethodNoSideEffect( isolate, templ, "exportECPrivateRaw", ExportECPrivateRaw); - SetProtoMethodNoSideEffect(isolate, templ, "rawSeed", RawSeed); - SetProtoMethod(isolate, templ, "initJwk", InitJWK); SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail); SetProtoMethod(isolate, templ, "equals", Equals); @@ -815,16 +1005,11 @@ void KeyObjectHandle::RegisterExternalReferences( registry->Register(CheckEcKeyData); registry->Register(Export); registry->Register(ExportJWK); - registry->Register(InitECRaw); - registry->Register(InitEDRaw); - registry->Register(InitPqcRaw); - registry->Register(InitECPrivateRaw); registry->Register(RawPublicKey); registry->Register(RawPrivateKey); + registry->Register(RawSeed); registry->Register(ExportECPublicRaw); registry->Register(ExportECPrivateRaw); - registry->Register(RawSeed); - registry->Register(InitJWK); registry->Register(GetKeyDetail); registry->Register(Equals); } @@ -865,6 +1050,7 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); MarkPopErrorOnReturn mark_pop_error_on_return; + Environment* env = Environment::GetCurrent(args); CHECK(args[0]->IsInt32()); KeyType type = static_cast(args[0].As()->Value()); @@ -872,25 +1058,70 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { switch (type) { case kKeyTypeSecret: { + if (args.Length() == 5 && args[2]->IsInt32()) { + auto format = static_cast( + args[2].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::JWK) { + CHECK(args[1]->IsObject()); + key->data_ = ImportJWKSecretKey(env, args[1].As()); + break; + } + } CHECK_EQ(args.Length(), 2); ArrayBufferOrViewContents buf(args[1]); key->data_ = KeyObjectData::CreateSecret(buf.ToCopy()); break; } - case kKeyTypePublic: { - CHECK_EQ(args.Length(), 5); - - offset = 1; - auto data = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset); - if (!data) return; - key->data_ = data.addRefWithType(kKeyTypePublic); - break; - } + case kKeyTypePublic: case kKeyTypePrivate: { - CHECK_EQ(args.Length(), 5); + CHECK_EQ(args.Length(), 6); + + // Check if this is a raw or JWK format import: + // args: [keyType, buffer/object, formatInt, typeString/null, + // passphrase/null, namedCurve/null] + if (args[2]->IsInt32()) { + auto format = static_cast( + args[2].As()->Value()); + if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC || + format == EVPKeyPointer::PKFormatType::RAW_PRIVATE || + format == EVPKeyPointer::PKFormatType::RAW_SEED) { + auto data = ImportRawKeyFromArgs(args, 1); + if (!data) return; + if (type == kKeyTypePublic && data.GetKeyType() == kKeyTypePrivate) { + key->data_ = data.addRefWithType(kKeyTypePublic); + } else { + key->data_ = std::move(data); + } + break; + } + if (format == EVPKeyPointer::PKFormatType::JWK) { + CHECK(args[1]->IsObject()); + key->data_ = ImportJWKFromArgs(env, args[1].As()); + if (!key->data_) return; + if (type == kKeyTypePublic && + key->data_.GetKeyType() == kKeyTypePrivate) { + key->data_ = key->data_.addRefWithType(kKeyTypePublic); + } else if (type == kKeyTypePrivate && + key->data_.GetKeyType() == kKeyTypePublic) { + THROW_ERR_CRYPTO_INVALID_JWK( + env, "JWK does not contain private key material"); + return; + } + args.GetReturnValue().Set(key->data_.GetKeyType()); + break; + } + } + offset = 1; - if (auto data = KeyObjectData::GetPrivateKeyFromJs(args, &offset, false)) { - key->data_ = std::move(data); + if (type == kKeyTypePublic) { + auto data = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!data) return; + key->data_ = data.addRefWithType(kKeyTypePublic); + } else { + if (auto data = + KeyObjectData::GetPrivateKeyFromJs(args, &offset, false)) { + key->data_ = std::move(data); + } } break; } @@ -899,192 +1130,6 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { } } -void KeyObjectHandle::InitJWK(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); - MarkPopErrorOnReturn mark_pop_error_on_return; - - // The argument must be a JavaScript object that we will inspect - // to get the JWK properties from. - CHECK(args[0]->IsObject()); - - // Step one, Secret key or not? - Local input = args[0].As(); - - Local kty; - if (!input->Get(env->context(), env->jwk_kty_string()).ToLocal(&kty) || - !kty->IsString()) { - return THROW_ERR_CRYPTO_INVALID_JWK(env); - } - - Utf8Value kty_string(env->isolate(), kty); - - if (kty_string == "oct") { - // Secret key - key->data_ = ImportJWKSecretKey(env, input); - if (!key->data_) { - // ImportJWKSecretKey is responsible for throwing an appropriate error - return; - } - } else { - key->data_ = ImportJWKAsymmetricKey(env, input, *kty_string, args, 1); - if (!key->data_) { - // ImportJWKAsymmetricKey is responsible for throwing an appropriate error - return; - } - } - - args.GetReturnValue().Set(key->data_.GetKeyType()); -} - -void KeyObjectHandle::InitECRaw(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); - - CHECK(args[0]->IsString()); - Utf8Value name(env->isolate(), args[0]); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - int id = ncrypto::Ec::GetCurveIdFromName(*name); - if (id == NID_undef) return THROW_ERR_CRYPTO_INVALID_CURVE(env); - auto eckey = ECKeyPointer::NewByCurveName(id); - if (!eckey) - return args.GetReturnValue().Set(false); - - const auto group = eckey.getGroup(); - auto pub = ECDH::BufferToPoint(env, group, args[1]); - - if (!pub || !eckey || !eckey.setPublicKey(pub)) { - return args.GetReturnValue().Set(false); - } - - auto pkey = EVPKeyPointer::New(); - if (!pkey.assign(eckey)) { - args.GetReturnValue().Set(false); - } - - eckey.release(); // Release ownership of the key - - key->data_ = KeyObjectData::CreateAsymmetric(kKeyTypePublic, std::move(pkey)); - - args.GetReturnValue().Set(true); -} - -void KeyObjectHandle::InitEDRaw(const FunctionCallbackInfo& args) { - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); - - CHECK(args[0]->IsString()); - Utf8Value name(args.GetIsolate(), args[0]); - - ArrayBufferOrViewContents key_data(args[1]); - KeyType type = FromV8Value(args[2]); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - typedef EVPKeyPointer (*new_key_fn)( - int, const ncrypto::Buffer&); - new_key_fn fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate - : EVPKeyPointer::NewRawPublic; - - int id = GetNidFromName(*name); - - switch (id) { - case EVP_PKEY_X25519: - case EVP_PKEY_X448: - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: { - auto pkey = fn(id, - ncrypto::Buffer{ - .data = key_data.data(), - .len = key_data.size(), - }); - if (!pkey) { - return args.GetReturnValue().Set(false); - } - key->data_ = KeyObjectData::CreateAsymmetric(type, std::move(pkey)); - CHECK(key->data_); - break; - } - default: - return args.GetReturnValue().Set(false); - } - - args.GetReturnValue().Set(true); -} - -void KeyObjectHandle::InitPqcRaw(const FunctionCallbackInfo& args) { -#if OPENSSL_WITH_PQC - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); - - CHECK(args[0]->IsString()); - Utf8Value name(args.GetIsolate(), args[0]); - - ArrayBufferOrViewContents key_data(args[1]); - KeyType type = FromV8Value(args[2]); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - int id = GetNidFromName(*name); - typedef EVPKeyPointer (*new_key_fn)( - int, const ncrypto::Buffer&); - new_key_fn fn = nullptr; - - switch (id) { - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawSeed - : EVPKeyPointer::NewRawPublic; - break; - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: - fn = type == kKeyTypePrivate ? EVPKeyPointer::NewRawPrivate - : EVPKeyPointer::NewRawPublic; - break; - default: - break; - } - - if (fn == nullptr) { - return args.GetReturnValue().Set(false); - } - - auto pkey = fn(id, - ncrypto::Buffer{ - .data = key_data.data(), - .len = key_data.size(), - }); - if (!pkey) { - return args.GetReturnValue().Set(false); - } - key->data_ = KeyObjectData::CreateAsymmetric(type, std::move(pkey)); - CHECK(key->data_); - - args.GetReturnValue().Set(true); -#else - Environment* env = Environment::GetCurrent(args); - THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); -#endif -} - void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; KeyObjectHandle* arg_handle; @@ -1174,45 +1219,12 @@ Local KeyObjectHandle::GetAsymmetricKeyType() const { case EVP_PKEY_X448: return env()->crypto_x448_string(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - return env()->crypto_ml_dsa_44_string(); - case EVP_PKEY_ML_DSA_65: - return env()->crypto_ml_dsa_65_string(); - case EVP_PKEY_ML_DSA_87: - return env()->crypto_ml_dsa_87_string(); - case EVP_PKEY_ML_KEM_512: - return env()->crypto_ml_kem_512_string(); - case EVP_PKEY_ML_KEM_768: - return env()->crypto_ml_kem_768_string(); - case EVP_PKEY_ML_KEM_1024: - return env()->crypto_ml_kem_1024_string(); - case EVP_PKEY_SLH_DSA_SHA2_128F: - return env()->crypto_slh_dsa_sha2_128f_string(); - case EVP_PKEY_SLH_DSA_SHA2_128S: - return env()->crypto_slh_dsa_sha2_128s_string(); - case EVP_PKEY_SLH_DSA_SHA2_192F: - return env()->crypto_slh_dsa_sha2_192f_string(); - case EVP_PKEY_SLH_DSA_SHA2_192S: - return env()->crypto_slh_dsa_sha2_192s_string(); - case EVP_PKEY_SLH_DSA_SHA2_256F: - return env()->crypto_slh_dsa_sha2_256f_string(); - case EVP_PKEY_SLH_DSA_SHA2_256S: - return env()->crypto_slh_dsa_sha2_256s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128F: - return env()->crypto_slh_dsa_shake_128f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_128S: - return env()->crypto_slh_dsa_shake_128s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192F: - return env()->crypto_slh_dsa_shake_192f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_192S: - return env()->crypto_slh_dsa_shake_192s_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256F: - return env()->crypto_slh_dsa_shake_256f_string(); - case EVP_PKEY_SLH_DSA_SHAKE_256S: - return env()->crypto_slh_dsa_shake_256s_string(); -#endif + default: + return GetPqcAsymmetricKeyType(env(), data_.GetAsymmetricKey().id()); +#else default: return Undefined(env()->isolate()); +#endif } } @@ -1321,34 +1333,14 @@ void KeyObjectHandle::RawPublicKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + is_raw_supported = is_raw_supported || IsPqcKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPublicKey(); @@ -1374,28 +1366,14 @@ void KeyObjectHandle::RawPrivateKey( Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: - case EVP_PKEY_X25519: - case EVP_PKEY_X448: + const int id = pkey.id(); + bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || + id == EVP_PKEY_X25519 || id == EVP_PKEY_X448; #if OPENSSL_WITH_PQC - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + is_raw_supported = is_raw_supported || IsPqcRawPrivateKeyId(id); #endif - break; - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!is_raw_supported) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } auto raw_data = pkey.rawPrivateKey(); @@ -1476,59 +1454,6 @@ void KeyObjectHandle::ExportECPrivateRaw( .FromMaybe(Local())); } -void KeyObjectHandle::InitECPrivateRaw( - const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - KeyObjectHandle* key; - ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); - - CHECK(args[0]->IsString()); - Utf8Value name(env->isolate(), args[0]); - - ArrayBufferOrViewContents key_data(args[1]); - - MarkPopErrorOnReturn mark_pop_error_on_return; - - int nid = ncrypto::Ec::GetCurveIdFromName(*name); - if (nid == NID_undef) return THROW_ERR_CRYPTO_INVALID_CURVE(env); - - auto eckey = ECKeyPointer::NewByCurveName(nid); - if (!eckey) return args.GetReturnValue().Set(false); - - // Validate key data size matches the curve's expected private key length - const auto group = eckey.getGroup(); - auto order = BignumPointer::New(); - CHECK(order); - CHECK(EC_GROUP_get_order(group, order.get(), nullptr)); - if (key_data.size() != order.byteLength()) - return args.GetReturnValue().Set(false); - - BignumPointer priv_bn(key_data.data(), key_data.size()); - if (!priv_bn) return args.GetReturnValue().Set(false); - - if (!eckey.setPrivateKey(priv_bn)) return args.GetReturnValue().Set(false); - - // Compute public key from private key - auto pub_point = ECPointPointer::New(group); - if (!pub_point || !pub_point.mul(group, priv_bn.get())) { - return args.GetReturnValue().Set(false); - } - - if (!eckey.setPublicKey(pub_point)) return args.GetReturnValue().Set(false); - - auto pkey = EVPKeyPointer::New(); - if (!pkey.assign(eckey)) { - return args.GetReturnValue().Set(false); - } - - eckey.release(); - - key->data_ = - KeyObjectData::CreateAsymmetric(kKeyTypePrivate, std::move(pkey)); - - args.GetReturnValue().Set(true); -} - void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); KeyObjectHandle* key; @@ -1537,24 +1462,14 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { const KeyObjectData& data = key->Data(); CHECK_EQ(data.GetKeyType(), kKeyTypePrivate); +#if OPENSSL_WITH_PQC Mutex::ScopedLock lock(data.mutex()); const auto& pkey = data.GetAsymmetricKey(); - switch (pkey.id()) { -#if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_ML_KEM_512: - case EVP_PKEY_ML_KEM_768: - case EVP_PKEY_ML_KEM_1024: - break; -#endif - default: - return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + if (!IsPqcSeedKeyId(pkey.id())) { + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } -#if OPENSSL_WITH_PQC auto raw_data = pkey.rawSeed(); if (!raw_data) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to get raw seed"); @@ -1563,6 +1478,8 @@ void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo& args) { args.GetReturnValue().Set( Buffer::Copy(env, raw_data.get(), raw_data.size()) .FromMaybe(Local())); +#else + return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); #endif } @@ -1585,18 +1502,26 @@ void NativeKeyObject::Initialize(Environment* env, Local target) { target, "createNativeKeyObjectClass", NativeKeyObject::CreateNativeKeyObjectClass); + SetMethod( + env->context(), target, "getKeyObjectSlots", NativeKeyObject::GetSlots); } void NativeKeyObject::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(NativeKeyObject::CreateNativeKeyObjectClass); + registry->Register(NativeKeyObject::GetSlots); registry->Register(NativeKeyObject::New); } +bool NativeKeyObject::HasInstance(Environment* env, Local value) { + auto t = env->crypto_key_object_constructor_template(); + return !t.IsEmpty() && t->HasInstance(value); +} + void NativeKeyObject::New(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK_EQ(args.Length(), 1); - CHECK(args[0]->IsObject()); + CHECK(KeyObjectHandle::HasInstance(env, args[0])); KeyObjectHandle* handle = Unwrap(args[0].As()); CHECK_NOT_NULL(handle); new NativeKeyObject(env, args.This(), handle->Data()); @@ -1615,6 +1540,8 @@ void NativeKeyObject::CreateNativeKeyObjectClass( NewFunctionTemplate(isolate, NativeKeyObject::New); t->InstanceTemplate()->SetInternalFieldCount( NativeKeyObject::kInternalFieldCount); + CHECK(env->crypto_key_object_constructor_template().IsEmpty()); + env->set_crypto_key_object_constructor_template(t); Local ctor; if (!t->GetFunction(env->context()).ToLocal(&ctor)) @@ -1636,6 +1563,34 @@ void NativeKeyObject::CreateNativeKeyObjectClass( args.GetReturnValue().Set(ret); } +// Returns the key's native hidden slot tuple as a single Array: +// [type enum, handle]. JS-side helpers call this once per key to prime +// a per-instance cache; derived metadata is appended lazily from JS by +// calling methods on the returned KeyObjectHandle. +void NativeKeyObject::GetSlots(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + if (!HasInstance(env, args[0])) { + THROW_ERR_INVALID_THIS(env, "Value of \"this\" must be of type KeyObject"); + return; + } + + NativeKeyObject* native = Unwrap(args[0].As()); + CHECK_NOT_NULL(native); + + Local handle; + if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) { + return; + } + + Isolate* isolate = env->isolate(); + Local slots[] = { + Uint32::NewFromUnsigned(isolate, native->handle_data_.GetKeyType()), + handle, + }; + args.GetReturnValue().Set(Array::New(isolate, slots, arraysize(slots))); +} + BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( Environment* env, Local context, @@ -1675,7 +1630,7 @@ BaseObjectPtr NativeKeyObject::KeyObjectTransferData::Deserialize( if (!key_ctor->NewInstance(context, 1, &handle).ToLocal(&key)) return {}; - return BaseObjectPtr(Unwrap(key.As())); + return BaseObjectPtr(Unwrap(key.As())); } BaseObject::TransferMode NativeKeyObject::GetTransferMode() const { @@ -1713,6 +1668,303 @@ WebCryptoKeyExportStatus PKEY_PKCS8_Export(const KeyObjectData& key_data, return WebCryptoKeyExportStatus::OK; } +void NativeCryptoKey::Initialize(Environment* env, Local target) { + SetMethod(env->context(), + target, + "createCryptoKeyClass", + NativeCryptoKey::CreateCryptoKeyClass); + SetMethod( + env->context(), target, "getCryptoKeySlots", NativeCryptoKey::GetSlots); +} + +void NativeCryptoKey::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(NativeCryptoKey::CreateCryptoKeyClass); + registry->Register(NativeCryptoKey::GetSlots); + registry->Register(NativeCryptoKey::New); +} + +namespace { +// Verifies that `value` is a `NativeCryptoKey` by checking whether it +// was constructed from the Environment's `NativeCryptoKey` template. +bool IsNativeCryptoKey(Environment* env, Local value) { + auto t = env->crypto_cryptokey_constructor_template(); + return !t.IsEmpty() && t->HasInstance(value); +} +} // namespace + +bool NativeCryptoKey::HasInstance(Environment* env, Local value) { + return IsNativeCryptoKey(env, value); +} + +MaybeLocal NativeCryptoKey::Create(Environment* env, + const KeyObjectData& data, + Local algorithm, + uint32_t usages_mask, + bool extractable) { + Local context = env->context(); + Isolate* isolate = env->isolate(); + CHECK(algorithm->IsObject()); + + Local handle; + if (!KeyObjectHandle::Create(env, data).ToLocal(&handle)) return {}; + + if (env->crypto_internal_cryptokey_constructor().IsEmpty()) { + Local arg = FIXED_ONE_BYTE_STRING(isolate, "internal/crypto/keys"); + if (env->builtin_module_require() + ->Call(context, Null(isolate), 1, &arg) + .IsEmpty()) { + return {}; + } + } + + Local cryptokey_ctor = env->crypto_internal_cryptokey_constructor(); + CHECK(!cryptokey_ctor.IsEmpty()); + Local ctor_args[] = { + handle, + algorithm, + Uint32::NewFromUnsigned(isolate, usages_mask), + Boolean::New(isolate, extractable), + }; + return cryptokey_ctor->NewInstance(context, arraysize(ctor_args), ctor_args); +} + +void NativeCryptoKey::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 4); + // args[0] is a KeyObjectHandle; we keep its KeyObjectData directly. + // args[1] is the algorithm dictionary object. + // args[2] is the usages mask. + // args[3] is the extractable boolean. + // + // args[1] is undefined only when called from + // CryptoKeyTransferData::Deserialize for a partially-initialized + // CryptoKey: algorithm/usages mask/extractable get filled in afterwards + // by FinalizeTransferRead before any JS can see the object. + // + // This constructor is not exposed to user JS - the public CryptoKey + // class throws from its constructor and InternalCryptoKey is kept + // in a module-closure. + CHECK(KeyObjectHandle::HasInstance(env, args[0])); + KeyObjectHandle* handle = Unwrap(args[0].As()); + CHECK_NOT_NULL(handle); + + auto* native = new NativeCryptoKey(env, args.This(), handle->Data()); + + if (!args[1]->IsUndefined()) { + CHECK(args[1]->IsObject()); + CHECK(args[2]->IsUint32()); + CHECK(args[3]->IsBoolean()); + args.This()->SetInternalField(kAlgorithmField, args[1]); + native->usages_mask_ = args[2].As()->Value(); + native->extractable_ = args[3]->IsTrue(); + } +} + +void NativeCryptoKey::CreateCryptoKeyClass( + const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + CHECK_EQ(args.Length(), 1); + Local callback = args[0]; + CHECK(callback->IsFunction()); + + Local t = + NewFunctionTemplate(isolate, NativeCryptoKey::New); + t->InstanceTemplate()->SetInternalFieldCount( + NativeCryptoKey::kInternalFieldCount); + CHECK(env->crypto_cryptokey_constructor_template().IsEmpty()); + env->set_crypto_cryptokey_constructor_template(t); + + Local ctor; + if (!t->GetFunction(env->context()).ToLocal(&ctor)) return; + + Local recv = Undefined(env->isolate()); + Local ret_v; + if (!callback.As() + ->Call(env->context(), recv, 1, &ctor) + .ToLocal(&ret_v)) { + return; + } + Local ret = ret_v.As(); + Local internal_ctor_v; + if (!ret->Get(env->context(), 1).ToLocal(&internal_ctor_v)) return; + CHECK(env->crypto_internal_cryptokey_constructor().IsEmpty()); + env->set_crypto_internal_cryptokey_constructor( + internal_ctor_v.As()); + args.GetReturnValue().Set(ret); +} + +// Returns all of the key's internal slot values as a single Array: +// [type enum, extractable, algorithm, usages mask, handle]. JS-side helpers +// call this once per key to prime a per-instance cache, so subsequent +// reads don't need to cross into C++ at all. +void NativeCryptoKey::GetSlots(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 1); + if (!HasInstance(env, args[0])) { + THROW_ERR_INVALID_THIS(env, "Value of \"this\" must be of type CryptoKey"); + return; + } + Local obj = args[0].As(); + NativeCryptoKey* native = Unwrap(obj); + CHECK_NOT_NULL(native); + + Local handle; + if (!KeyObjectHandle::Create(env, native->handle_data_).ToLocal(&handle)) { + return; + } + + Local algorithm = obj->GetInternalField(kAlgorithmField).As(); + CHECK(algorithm->IsObject()); + Isolate* isolate = env->isolate(); + Local slots[] = { + Uint32::NewFromUnsigned(isolate, native->handle_data_.GetKeyType()), + v8::Boolean::New(isolate, native->extractable_), + algorithm, + Uint32::NewFromUnsigned(isolate, native->usages_mask_), + handle, + }; + args.GetReturnValue().Set(Array::New(isolate, slots, arraysize(slots))); +} + +BaseObject::TransferMode NativeCryptoKey::GetTransferMode() const { + return BaseObject::TransferMode::kCloneable; +} + +std::unique_ptr NativeCryptoKey::CloneForMessaging() + const { + Isolate* isolate = env()->isolate(); + Local obj = object(); + Local algorithm_v = obj->GetInternalField(kAlgorithmField).As(); + CHECK(algorithm_v->IsObject()); + v8::Global algorithm_copy(isolate, algorithm_v.As()); + return std::make_unique( + handle_data_, std::move(algorithm_copy), usages_mask_, extractable_); +} + +Maybe NativeCryptoKey::FinalizeTransferRead( + Local context, v8::ValueDeserializer* deserializer) { + Local bundle_v; + if (!deserializer->ReadValue(context).ToLocal(&bundle_v)) { + return Nothing(); + } + CHECK(bundle_v->IsObject()); + Local bundle = bundle_v.As(); + Isolate* isolate = env()->isolate(); + Local obj = object(); + + // The partially-initialized object produced by + // CryptoKeyTransferData::Deserialize should not have algorithm set yet. + CHECK(obj->GetInternalField(kAlgorithmField).As()->IsUndefined()); + + Local algorithm_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "algorithm")) + .ToLocal(&algorithm_v)) { + return Nothing(); + } + CHECK(algorithm_v->IsObject()); + obj->SetInternalField(kAlgorithmField, algorithm_v); + + Local usages_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "usages")) + .ToLocal(&usages_v)) { + return Nothing(); + } + CHECK(usages_v->IsUint32()); + usages_mask_ = usages_v.As()->Value(); + + Local extractable_v; + if (!bundle->Get(context, FIXED_ONE_BYTE_STRING(isolate, "extractable")) + .ToLocal(&extractable_v)) { + return Nothing(); + } + CHECK(extractable_v->IsBoolean()); + extractable_ = extractable_v->IsTrue(); + + return v8::JustVoid(); +} + +Maybe NativeCryptoKey::CryptoKeyTransferData::FinalizeTransferWrite( + Local context, v8::ValueSerializer* serializer) { + Isolate* isolate = Isolate::GetCurrent(); + CHECK(!algorithm_.IsEmpty()); + Local bundle = Object::New(isolate); + Local algorithm_v = PersistentToLocal::Strong(algorithm_); + if (bundle + ->Set( + context, FIXED_ONE_BYTE_STRING(isolate, "algorithm"), algorithm_v) + .IsNothing() || + bundle + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "usages"), + Uint32::NewFromUnsigned(isolate, usages_mask_)) + .IsNothing() || + bundle + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "extractable"), + v8::Boolean::New(isolate, extractable_)) + .IsNothing()) { + return Nothing(); + } + auto ret = serializer->WriteValue(context, bundle); + algorithm_.Reset(); + return ret; +} + +BaseObjectPtr NativeCryptoKey::CryptoKeyTransferData::Deserialize( + Environment* env, + Local context, + std::unique_ptr self) { + if (context != env->context()) { + THROW_ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE(env); + return {}; + } + + // Reconstruct the KeyObjectHandle for the transferred KeyObjectData. + Local handle; + if (!KeyObjectHandle::Create(env, data_).ToLocal(&handle)) return {}; + + // Make sure internal/crypto/keys has been loaded so that the + // CryptoKey constructor is registered with the Environment. + Isolate* isolate = env->isolate(); + Local arg = FIXED_ONE_BYTE_STRING(isolate, "internal/crypto/keys"); + if (env->builtin_module_require() + ->Call(context, Null(isolate), 1, &arg) + .IsEmpty()) { + return {}; + } + + // Construct a partially-initialized InternalCryptoKey; algorithm, + // usages mask and extractable are filled in via FinalizeTransferRead. + Local cryptokey_ctor = env->crypto_internal_cryptokey_constructor(); + CHECK(!cryptokey_ctor.IsEmpty()); + Local ctor_args[] = { + handle, + Undefined(isolate), + Undefined(isolate), + Undefined(isolate), + }; + Local cryptokey; + if (!cryptokey_ctor->NewInstance(context, 4, ctor_args).ToLocal(&cryptokey)) { + return {}; + } + + return BaseObjectPtr( + Unwrap(cryptokey.As())); +} + +void NativeCryptoKey::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("handle_data", handle_data_); +} + +void NativeCryptoKey::CryptoKeyTransferData::MemoryInfo( + MemoryTracker* tracker) const { + tracker->TrackField("data", data_); + tracker->TrackField("algorithm", algorithm_); +} + namespace Keys { void Initialize(Environment* env, Local target) { target->Set(env->context(), @@ -1753,9 +2005,12 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_44); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_65); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_DSA_87); +#if OPENSSL_WITH_PQC_ML_KEM_512 NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_512); +#endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_768); NODE_DEFINE_CONSTANT(target, EVP_PKEY_ML_KEM_1024); +#if OPENSSL_WITH_PQC_SLH_DSA NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_128S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHA2_192F); @@ -1768,6 +2023,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_192S); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256F); NODE_DEFINE_CONSTANT(target, EVP_PKEY_SLH_DSA_SHAKE_256S); +#endif #endif NODE_DEFINE_CONSTANT(target, EVP_PKEY_X25519); NODE_DEFINE_CONSTANT(target, EVP_PKEY_X448); diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index d35249724a448e..b91a69bb048783 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -13,6 +13,7 @@ #include +#include #include #include @@ -150,9 +151,6 @@ class KeyObjectHandle : public BaseObject { static void New(const v8::FunctionCallbackInfo& args); static void Init(const v8::FunctionCallbackInfo& args); - static void InitECRaw(const v8::FunctionCallbackInfo& args); - static void InitEDRaw(const v8::FunctionCallbackInfo& args); - static void InitJWK(const v8::FunctionCallbackInfo& args); static void GetKeyDetail(const v8::FunctionCallbackInfo& args); static void Equals(const v8::FunctionCallbackInfo& args); @@ -176,8 +174,6 @@ class KeyObjectHandle : public BaseObject { const v8::FunctionCallbackInfo& args); static void ExportECPrivateRaw( const v8::FunctionCallbackInfo& args); - static void InitECPrivateRaw(const v8::FunctionCallbackInfo& args); - static void InitPqcRaw(const v8::FunctionCallbackInfo& args); static void RawSeed(const v8::FunctionCallbackInfo& args); v8::MaybeLocal ExportSecretKey() const; @@ -193,6 +189,11 @@ class KeyObjectHandle : public BaseObject { KeyObjectData data_; }; +// NativeKeyObject is the native base class for the Node.js-specific +// `KeyObject`. It holds the underlying KeyObjectData for structured +// cloning and exposes the native hidden slot tuple that JS needs: +// [type enum, KeyObjectHandle]. JS primes a per-instance private-field +// cache from that result and lazily appends derived metadata there. class NativeKeyObject : public BaseObject { public: static void Initialize(Environment* env, v8::Local target); @@ -202,6 +203,15 @@ class NativeKeyObject : public BaseObject { static void CreateNativeKeyObjectClass( const v8::FunctionCallbackInfo& args); + // True if `value` is a real NativeKeyObject instance. Uses the + // FunctionTemplate stored on the Environment as a brand check. + // Used by `GetSlots` to validate its receiver. + static bool HasInstance(Environment* env, v8::Local value); + + // Returns [type, handle] in one call so JS can prime a per-instance cache + // on first access. Derived metadata is not returned from native here. + static void GetSlots(const v8::FunctionCallbackInfo& args); + SET_NO_MEMORY_INFO() SET_MEMORY_INFO_NAME(NativeKeyObject) SET_SELF_SIZE(NativeKeyObject) @@ -238,6 +248,100 @@ class NativeKeyObject : public BaseObject { KeyObjectData handle_data_; }; +// NativeCryptoKey is the native base class for the Web Crypto +// `CryptoKey`. It holds the internal slots - `[[type]]` as an enum, +// `[[extractable]]`, `[[algorithm]]`, `[[usages]]` as a mask, and the +// underlying KeyObjectData. The public `type`, `extractable`, +// `algorithm`, and `usages` accessors on `CryptoKey.prototype` are +// user-configurable per Web IDL, so internal consumers read these +// values directly from the C++ side via a single `GetSlots` call +// which returns all slots at once; JS primes a per-instance cache +// from that result. +class NativeCryptoKey : public BaseObject { + public: + enum InternalFields { + kAlgorithmField = BaseObject::kInternalFieldCount, + kInternalFieldCount, + }; + + static void Initialize(Environment* env, v8::Local target); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + static void New(const v8::FunctionCallbackInfo& args); + static void CreateCryptoKeyClass( + const v8::FunctionCallbackInfo& args); + + static v8::MaybeLocal Create(Environment* env, + const KeyObjectData& data, + v8::Local algorithm, + uint32_t usages_mask, + bool extractable); + + // True if `value` is a real NativeCryptoKey instance. Uses the + // FunctionTemplate stored on the Environment as a brand check. + // Used by `GetSlots` to validate its receiver. + static bool HasInstance(Environment* env, v8::Local value); + + // Returns [type, extractable, algorithm, usages mask, handle] in one call + // so JS can prime a per-instance cache on first access. + static void GetSlots(const v8::FunctionCallbackInfo& args); + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(NativeCryptoKey) + SET_SELF_SIZE(NativeCryptoKey) + + class CryptoKeyTransferData : public worker::TransferData { + public: + CryptoKeyTransferData(const KeyObjectData& data, + v8::Global&& algorithm, + uint32_t usages_mask, + bool extractable) + : data_(data.addRef()), + algorithm_(std::move(algorithm)), + usages_mask_(usages_mask), + extractable_(extractable) {} + + BaseObjectPtr Deserialize( + Environment* env, + v8::Local context, + std::unique_ptr self) override; + + v8::Maybe FinalizeTransferWrite( + v8::Local context, + v8::ValueSerializer* serializer) override; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CryptoKeyTransferData) + SET_SELF_SIZE(CryptoKeyTransferData) + + private: + KeyObjectData data_; + v8::Global algorithm_; + uint32_t usages_mask_; + bool extractable_; + }; + + BaseObject::TransferMode GetTransferMode() const override; + std::unique_ptr CloneForMessaging() const override; + v8::Maybe FinalizeTransferRead( + v8::Local context, + v8::ValueDeserializer* deserializer) override; + + const KeyObjectData& handle_data() const { return handle_data_; } + + private: + NativeCryptoKey(Environment* env, + v8::Local wrap, + const KeyObjectData& handle_data) + : BaseObject(env, wrap), handle_data_(handle_data.addRef()) { + MakeWeak(); + } + + KeyObjectData handle_data_; + uint32_t usages_mask_ = 0; + bool extractable_ = false; +}; + enum WebCryptoKeyFormat { kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index c862a20f410d9d..38aa6b66257d5e 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -3,7 +3,7 @@ #include "node_internals.h" #include "threadpoolwork-inl.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC #include #include #include "crypto/crypto_keys.h" @@ -45,7 +45,7 @@ KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { void KmacConfig::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); // If the job is sync, then the KmacConfig does not own the data. - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("customization", customization.size()); @@ -90,7 +90,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); return Nothing(); } - params->customization = mode == kCryptoJobAsync + params->customization = IsCryptoJobAsync(mode) ? customization.ToCopy() : customization.ToByteSource(); } @@ -104,7 +104,7 @@ Maybe KmacTraits::AdditionalConfig( THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync ? data.ToCopy() : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); if (!args[offset + 6]->IsUndefined()) { ArrayBufferOrViewContents signature(args[offset + 6]); @@ -113,7 +113,7 @@ Maybe KmacTraits::AdditionalConfig( return Nothing(); } params->signature = - mode == kCryptoJobAsync ? signature.ToCopy() : signature.ToByteSource(); + IsCryptoJobAsync(mode) ? signature.ToCopy() : signature.ToByteSource(); } return JustVoid(); @@ -218,4 +218,4 @@ void Kmac::RegisterExternalReferences(ExternalReferenceRegistry* registry) { } // namespace node::crypto -#endif +#endif // OPENSSL_WITH_KMAC diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 94dc40657b3819..c6e8a88c168efb 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -10,8 +10,7 @@ namespace node::crypto { -// KMAC (Keccak Message Authentication Code) is available since OpenSSL 3.0. -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KMAC enum class KmacVariant { KMAC128, KMAC256 }; @@ -71,7 +70,7 @@ namespace Kmac { void Initialize(Environment* env, v8::Local target) {} void RegisterExternalReferences(ExternalReferenceRegistry* registry) {} } // namespace Kmac -#endif +#endif // OPENSSL_WITH_KMAC } // namespace node::crypto diff --git a/src/crypto/crypto_ml_dsa.cc b/src/crypto/crypto_ml_dsa.cc deleted file mode 100644 index 65f7053cc1fa1d..00000000000000 --- a/src/crypto/crypto_ml_dsa.cc +++ /dev/null @@ -1,85 +0,0 @@ -#include "crypto/crypto_ml_dsa.h" -#include "crypto/crypto_util.h" -#include "env-inl.h" -#include "string_bytes.h" -#include "v8.h" - -namespace node { - -using ncrypto::DataPointer; -using v8::Local; -using v8::Object; -using v8::String; -using v8::Value; - -namespace crypto { - -#if OPENSSL_WITH_PQC -constexpr const char* GetMlDsaAlgorithmName(int id) { - switch (id) { - case EVP_PKEY_ML_DSA_44: - return "ML-DSA-44"; - case EVP_PKEY_ML_DSA_65: - return "ML-DSA-65"; - case EVP_PKEY_ML_DSA_87: - return "ML-DSA-87"; - default: - return nullptr; - } -} - -/** - * Exports an ML-DSA key to JWK format. - * - * The resulting JWK object contains: - * - "kty": "AKP" (Asymmetric Key Pair - required) - * - "alg": "ML-DSA-XX" (Algorithm identifier - required for "AKP") - * - "pub": "" (required) - * - "priv": <"Base64URL-encoded raw seed>" (required for private keys only) - */ -bool ExportJwkMlDsaKey(Environment* env, - const KeyObjectData& key, - Local target) { - Mutex::ScopedLock lock(key.mutex()); - const auto& pkey = key.GetAsymmetricKey(); - - const char* alg = GetMlDsaAlgorithmName(pkey.id()); - CHECK(alg); - - static constexpr auto trySetKey = [](Environment* env, - DataPointer data, - Local target, - Local key) { - Local encoded; - if (!data) return false; - const ncrypto::Buffer out = data; - return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) - .ToLocal(&encoded) && - target->Set(env->context(), key, encoded).IsJust(); - }; - - if (key.GetKeyType() == kKeyTypePrivate) { - auto seed = pkey.rawSeed(); - if (!seed) { - THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "key does not have an available seed"); - return false; - } - if (!trySetKey(env, pkey.rawSeed(), target, env->jwk_priv_string())) { - return false; - } - } - - return !( - target->Set(env->context(), env->jwk_kty_string(), env->jwk_akp_string()) - .IsNothing() || - target - ->Set(env->context(), - env->jwk_alg_string(), - OneByteString(env->isolate(), alg)) - .IsNothing() || - !trySetKey(env, pkey.rawPublicKey(), target, env->jwk_pub_string())); -} -#endif -} // namespace crypto -} // namespace node diff --git a/src/crypto/crypto_ml_dsa.h b/src/crypto/crypto_ml_dsa.h deleted file mode 100644 index e4739fcdd7fda7..00000000000000 --- a/src/crypto/crypto_ml_dsa.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef SRC_CRYPTO_CRYPTO_ML_DSA_H_ -#define SRC_CRYPTO_CRYPTO_ML_DSA_H_ - -#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS - -#include "crypto/crypto_keys.h" -#include "env.h" -#include "v8.h" - -namespace node { -namespace crypto { -#if OPENSSL_WITH_PQC -bool ExportJwkMlDsaKey(Environment* env, - const KeyObjectData& key, - v8::Local target); -#endif -} // namespace crypto -} // namespace node - -#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#endif // SRC_CRYPTO_CRYPTO_ML_DSA_H_ diff --git a/src/crypto/crypto_pbkdf2.cc b/src/crypto/crypto_pbkdf2.cc index 83e7cda48e46ee..3f543392790d14 100644 --- a/src/crypto/crypto_pbkdf2.cc +++ b/src/crypto/crypto_pbkdf2.cc @@ -1,5 +1,7 @@ #include "crypto/crypto_pbkdf2.h" #include "async_wrap-inl.h" +#include "base_object-inl.h" +#include "crypto/crypto_keys.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -21,6 +23,7 @@ using v8::Value; namespace crypto { PBKDF2Config::PBKDF2Config(PBKDF2Config&& other) noexcept : mode(other.mode), + key(std::move(other.key)), pass(std::move(other.pass)), salt(std::move(other.salt)), iterations(other.iterations), @@ -34,9 +37,10 @@ PBKDF2Config& PBKDF2Config::operator=(PBKDF2Config&& other) noexcept { } void PBKDF2Config::MemoryInfo(MemoryTracker* tracker) const { - // The job is sync, the PBKDF2Config does not own the data. - if (mode == kCryptoJobAsync) { - tracker->TrackFieldWithSize("pass", pass.size()); + // If the job is sync, PBKDF2Config does not own the data. + if (key) tracker->TrackField("key", key); + if (IsCryptoJobAsync(mode)) { + if (!key) tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } } @@ -63,12 +67,21 @@ Maybe PBKDF2Traits::AdditionalConfig( params->mode = mode; - ArrayBufferOrViewContents pass(args[offset]); + CHECK(KeyObjectHandle::HasInstance(env, args[offset]) || + IsAnyBufferSource(args[offset])); // pass ArrayBufferOrViewContents salt(args[offset + 1]); - if (!pass.CheckSizeInt32()) [[unlikely]] { - THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); - return Nothing(); + if (KeyObjectHandle::HasInstance(env, args[offset])) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args[offset], Nothing()); + params->key = key->Data().addRef(); + } else { + ArrayBufferOrViewContents pass(args[offset]); + if (!pass.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "pass is too large"); + return Nothing(); + } + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); } if (!salt.CheckSizeInt32()) [[unlikely]] { @@ -76,13 +89,7 @@ Maybe PBKDF2Traits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); - - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsInt32()); // iteration_count CHECK(args[offset + 3]->IsInt32()); // length @@ -115,11 +122,14 @@ bool PBKDF2Traits::DeriveBits(Environment* env, ByteSource* out, CryptoJobMode mode) { // Both pass and salt may be zero length here. + const ncrypto::Buffer pass{ + .data = params.key ? params.key.GetSymmetricKey() + : params.pass.data(), + .len = params.key ? params.key.GetSymmetricKeySize() : params.pass.size(), + }; + auto dp = ncrypto::pbkdf2(params.digest, - ncrypto::Buffer{ - .data = params.pass.data(), - .len = params.pass.size(), - }, + pass, ncrypto::Buffer{ .data = params.salt.data(), .len = params.salt.size(), diff --git a/src/crypto/crypto_pbkdf2.h b/src/crypto/crypto_pbkdf2.h index e87447e381b80b..05f3dcb968794f 100644 --- a/src/crypto/crypto_pbkdf2.h +++ b/src/crypto/crypto_pbkdf2.h @@ -3,8 +3,9 @@ #if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS -#include "crypto/crypto_util.h" #include "async_wrap.h" +#include "crypto/crypto_keys.h" +#include "crypto/crypto_util.h" #include "env.h" #include "memory_tracker.h" #include "v8.h" @@ -26,6 +27,7 @@ namespace crypto { struct PBKDF2Config final : public MemoryRetainer { CryptoJobMode mode; + KeyObjectData key; ByteSource pass; ByteSource salt; int32_t iterations; diff --git a/src/crypto/crypto_pqc.cc b/src/crypto/crypto_pqc.cc new file mode 100644 index 00000000000000..3cccca0c0d739d --- /dev/null +++ b/src/crypto/crypto_pqc.cc @@ -0,0 +1,283 @@ +#include "crypto/crypto_pqc.h" +#include "crypto/crypto_util.h" +#include "env-inl.h" +#include "string_bytes.h" +#include "v8.h" + +namespace node { + +using ncrypto::DataPointer; +using ncrypto::EVPKeyPointer; +using v8::Local; +using v8::Object; +using v8::String; +using v8::Value; + +namespace crypto { + +#if OPENSSL_WITH_PQC +namespace { +using PqcKeyTypeGetter = Local (Environment::*)() const; + +enum PqcAlgorithmFlag { + kPqcRawPrivate = 1 << 0, + kPqcRawSeed = 1 << 1, + kPqcSignature = 1 << 2, +}; + +struct PqcAlgorithm { + int id; + const char* name; + PqcKeyTypeGetter key_type; + int flags; +}; + +// ML-DSA and ML-KEM carry private material as a seed. SLH-DSA uses the +// expanded private key and is only exposed by OpenSSL. +constexpr int kPqcMlDsaFlags = kPqcRawSeed | kPqcSignature; +constexpr int kPqcMlKemFlags = kPqcRawSeed; +constexpr int kPqcSlhDsaFlags = kPqcRawPrivate | kPqcSignature; + +constexpr PqcAlgorithm kPqcAlgorithms[] = { + {EVP_PKEY_ML_DSA_44, + "ML-DSA-44", + &Environment::crypto_ml_dsa_44_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_65, + "ML-DSA-65", + &Environment::crypto_ml_dsa_65_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_DSA_87, + "ML-DSA-87", + &Environment::crypto_ml_dsa_87_string, + kPqcMlDsaFlags}, + {EVP_PKEY_ML_KEM_768, + "ML-KEM-768", + &Environment::crypto_ml_kem_768_string, + kPqcMlKemFlags}, + {EVP_PKEY_ML_KEM_1024, + "ML-KEM-1024", + &Environment::crypto_ml_kem_1024_string, + kPqcMlKemFlags}, + +#if OPENSSL_WITH_PQC_ML_KEM_512 + {EVP_PKEY_ML_KEM_512, + "ML-KEM-512", + &Environment::crypto_ml_kem_512_string, + kPqcMlKemFlags}, +#endif +#if OPENSSL_WITH_PQC_SLH_DSA + {EVP_PKEY_SLH_DSA_SHA2_128F, + "SLH-DSA-SHA2-128f", + &Environment::crypto_slh_dsa_sha2_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_128S, + "SLH-DSA-SHA2-128s", + &Environment::crypto_slh_dsa_sha2_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192F, + "SLH-DSA-SHA2-192f", + &Environment::crypto_slh_dsa_sha2_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_192S, + "SLH-DSA-SHA2-192s", + &Environment::crypto_slh_dsa_sha2_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256F, + "SLH-DSA-SHA2-256f", + &Environment::crypto_slh_dsa_sha2_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHA2_256S, + "SLH-DSA-SHA2-256s", + &Environment::crypto_slh_dsa_sha2_256s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128F, + "SLH-DSA-SHAKE-128f", + &Environment::crypto_slh_dsa_shake_128f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_128S, + "SLH-DSA-SHAKE-128s", + &Environment::crypto_slh_dsa_shake_128s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192F, + "SLH-DSA-SHAKE-192f", + &Environment::crypto_slh_dsa_shake_192f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_192S, + "SLH-DSA-SHAKE-192s", + &Environment::crypto_slh_dsa_shake_192s_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256F, + "SLH-DSA-SHAKE-256f", + &Environment::crypto_slh_dsa_shake_256f_string, + kPqcSlhDsaFlags}, + {EVP_PKEY_SLH_DSA_SHAKE_256S, + "SLH-DSA-SHAKE-256s", + &Environment::crypto_slh_dsa_shake_256s_string, + kPqcSlhDsaFlags}, +#endif +}; + +const PqcAlgorithm* FindPqcAlgorithmById(int id) { + for (const auto& alg : kPqcAlgorithms) { + if (alg.id == id) return &alg; + } + return nullptr; +} + +const PqcAlgorithm* FindPqcAlgorithmByName(const char* name) { + for (const auto& alg : kPqcAlgorithms) { + if (strcmp(name, alg.name) == 0) return &alg; + } + return nullptr; +} + +bool HasPqcAlgorithmFlag(const PqcAlgorithm* alg, PqcAlgorithmFlag flag) { + return alg != nullptr && (alg->flags & flag) != 0; +} + +bool TrySetEncodedKey(Environment* env, + DataPointer data, + Local target, + Local key) { + Local encoded; + if (!data) return false; + const ncrypto::Buffer out = data; + return StringBytes::Encode(env->isolate(), out.data, out.len, BASE64URL) + .ToLocal(&encoded) && + target->DefineOwnProperty(env->context(), key, encoded) + .FromMaybe(false); +} +} // namespace + +bool ExportJwkPqcKey(Environment* env, + const KeyObjectData& key, + Local target) { + Mutex::ScopedLock lock(key.mutex()); + const auto& pkey = key.GetAsymmetricKey(); + + const PqcAlgorithm* alg = FindPqcAlgorithmById(pkey.id()); + CHECK(alg); + + if (key.GetKeyType() == kKeyTypePrivate) { + const bool uses_seed = HasPqcAlgorithmFlag(alg, kPqcRawSeed); + DataPointer priv_data = uses_seed ? pkey.rawSeed() : pkey.rawPrivateKey(); + if (uses_seed && !priv_data) { + THROW_ERR_CRYPTO_OPERATION_FAILED(env, + "key does not have an available seed"); + return false; + } + if (!TrySetEncodedKey( + env, std::move(priv_data), target, env->jwk_priv_string())) { + return false; + } + } + + return !(!target + ->DefineOwnProperty(env->context(), + env->jwk_kty_string(), + env->jwk_akp_string()) + .FromMaybe(false) || + !target + ->DefineOwnProperty(env->context(), + env->jwk_alg_string(), + OneByteString(env->isolate(), alg->name)) + .FromMaybe(false) || + !TrySetEncodedKey( + env, pkey.rawPublicKey(), target, env->jwk_pub_string())); +} + +KeyObjectData ImportJWKPqcKey(Environment* env, Local jwk) { + Local alg_value; + Local pub_value; + Local priv_value; + + if (!jwk->Get(env->context(), env->jwk_alg_string()).ToLocal(&alg_value) || + !jwk->Get(env->context(), env->jwk_pub_string()).ToLocal(&pub_value) || + !jwk->Get(env->context(), env->jwk_priv_string()).ToLocal(&priv_value)) { + return {}; + } + + Utf8Value alg_str(env->isolate(), + alg_value->IsString() ? alg_value.As() + : String::Empty(env->isolate())); + + const PqcAlgorithm* alg = FindPqcAlgorithmByName(*alg_str); + if (!alg) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Unsupported JWK AKP \"alg\""); + return {}; + } + + if (!pub_value->IsString() || + (!priv_value->IsUndefined() && !priv_value->IsString())) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK AKP key"); + return {}; + } + + KeyType type = priv_value->IsString() ? kKeyTypePrivate : kKeyTypePublic; + + EVPKeyPointer pkey; + if (type == kKeyTypePrivate) { + ByteSource priv = + ByteSource::FromEncodedString(env, priv_value.As()); + ncrypto::Buffer buf{ + .data = priv.data(), + .len = priv.size(), + }; + pkey = HasPqcAlgorithmFlag(alg, kPqcRawSeed) + ? EVPKeyPointer::NewRawSeed(alg->id, buf) + : EVPKeyPointer::NewRawPrivate(alg->id, buf); + } else { + ByteSource pub = ByteSource::FromEncodedString(env, pub_value.As()); + pkey = + EVPKeyPointer::NewRawPublic(alg->id, + ncrypto::Buffer{ + .data = pub.data(), + .len = pub.size(), + }); + } + + if (!pkey) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK AKP key"); + return {}; + } + + return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); +} + +bool IsPqcKeyId(int id) { + return FindPqcAlgorithmById(id) != nullptr; +} + +bool IsPqcRawPrivateKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return HasPqcAlgorithmFlag(alg, kPqcRawPrivate); +} + +bool IsPqcSeedKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return HasPqcAlgorithmFlag(alg, kPqcRawSeed); +} + +bool IsPqcSignatureKeyId(int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + return HasPqcAlgorithmFlag(alg, kPqcSignature); +} + +int GetPqcNidFromName(const char* name) { + for (const auto& alg : kPqcAlgorithms) { + if (StringEqualNoCase(name, alg.name)) return alg.id; + } + return NID_undef; +} + +Local GetPqcAsymmetricKeyType(Environment* env, int id) { + const PqcAlgorithm* alg = FindPqcAlgorithmById(id); + if (alg == nullptr) return v8::Undefined(env->isolate()); + + Local key_type = (env->*(alg->key_type))(); + return key_type.As(); +} +#endif +} // namespace crypto +} // namespace node diff --git a/src/crypto/crypto_pqc.h b/src/crypto/crypto_pqc.h new file mode 100644 index 00000000000000..14f919d94c6f8a --- /dev/null +++ b/src/crypto/crypto_pqc.h @@ -0,0 +1,39 @@ +#ifndef SRC_CRYPTO_CRYPTO_PQC_H_ +#define SRC_CRYPTO_CRYPTO_PQC_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_keys.h" +#include "env.h" +#include "v8.h" + +namespace node { +namespace crypto { +#if OPENSSL_WITH_PQC +bool ExportJwkPqcKey(Environment* env, + const KeyObjectData& key, + v8::Local target); + +KeyObjectData ImportJWKPqcKey(Environment* env, v8::Local jwk); + +// Returns true for PQC algorithms that support raw private key export/import. +bool IsPqcRawPrivateKeyId(int id); +// Returns true if the given EVP_PKEY id is a PQC algorithm known to Node. +bool IsPqcKeyId(int id); +// Returns true for PQC algorithms that carry the private key as a seed +// (ML-DSA, ML-KEM). Returns false for algorithms that use the expanded +// private key (SLH-DSA), or for non-PQC ids. +bool IsPqcSeedKeyId(int id); +// Returns true for PQC signature algorithms (ML-DSA, SLH-DSA). Returns false +// for ML-KEM or for non-PQC ids. +bool IsPqcSignatureKeyId(int id); +// Returns the EVP_PKEY id for the given PQC algorithm name, or NID_undef. +int GetPqcNidFromName(const char* name); +// Returns the JS asymmetricKeyType string for a PQC id, or undefined. +v8::Local GetPqcAsymmetricKeyType(Environment* env, int id); +#endif +} // namespace crypto +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_PQC_H_ diff --git a/src/crypto/crypto_rsa.cc b/src/crypto/crypto_rsa.cc index 62ee228945c45b..eced4175a3504f 100644 --- a/src/crypto/crypto_rsa.cc +++ b/src/crypto/crypto_rsa.cc @@ -128,13 +128,25 @@ Maybe RsaKeyGenTraits::AdditionalConfig( static_cast(args[*offset].As()->Value()); CHECK_IMPLIES(params->params.variant != kKeyVariantRSA_PSS, - args.Length() == 10); + static_cast(args.Length()) >= *offset + 3); CHECK_IMPLIES(params->params.variant == kKeyVariantRSA_PSS, - args.Length() == 13); + static_cast(args.Length()) >= *offset + 6); params->params.modulus_bits = args[*offset + 1].As()->Value(); params->params.exponent = args[*offset + 2].As()->Value(); +#ifdef OPENSSL_IS_BORINGSSL + // BoringSSL hangs indefinitely generating an RSA key with e=1, and for + // other invalid exponents (e=0, even values) reports the misleading error + // RSA_R_TOO_MANY_ITERATIONS only after running the full keygen loop. Reject + // those up-front with a clear error. The constraint here (odd integer >= 3) + // matches BoringSSL's own rsa_check_public_key validation. + if (params->params.exponent < 3 || (params->params.exponent & 1) == 0) { + THROW_ERR_OUT_OF_RANGE(env, "publicExponent is invalid"); + return Nothing(); + } +#endif + *offset += 3; if (params->params.variant == kKeyVariantRSA_PSS) { @@ -253,7 +265,7 @@ RSACipherConfig::RSACipherConfig(RSACipherConfig&& other) noexcept digest(other.digest) {} void RSACipherConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) + if (IsCryptoJobAsync(mode)) tracker->TrackFieldWithSize("label", label.size()); } @@ -325,8 +337,10 @@ bool ExportJWKRsaKey(Environment* env, const ncrypto::Rsa rsa = m_pkey; if (!rsa || - target->Set(env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) - .IsNothing()) { + !target + ->DefineOwnProperty( + env->context(), env->jwk_kty_string(), env->jwk_rsa_string()) + .FromMaybe(false)) { return false; } @@ -360,10 +374,7 @@ bool ExportJWKRsaKey(Environment* env, return true; } -KeyObjectData ImportJWKRsaKey(Environment* env, - Local jwk, - const FunctionCallbackInfo& args, - unsigned int offset) { +KeyObjectData ImportJWKRsaKey(Environment* env, Local jwk) { Local n_value; Local e_value; Local d_value; diff --git a/src/crypto/crypto_rsa.h b/src/crypto/crypto_rsa.h index a9912d6f43674b..61279c9a18b34e 100644 --- a/src/crypto/crypto_rsa.h +++ b/src/crypto/crypto_rsa.h @@ -116,10 +116,7 @@ bool ExportJWKRsaKey(Environment* env, const KeyObjectData& key, v8::Local target); -KeyObjectData ImportJWKRsaKey(Environment* env, - v8::Local jwk, - const v8::FunctionCallbackInfo& args, - unsigned int offset); +KeyObjectData ImportJWKRsaKey(Environment* env, v8::Local jwk); bool GetRsaKeyDetail(Environment* env, const KeyObjectData& key, diff --git a/src/crypto/crypto_scrypt.cc b/src/crypto/crypto_scrypt.cc index 27fe25a2457ae3..601a573e4fc619 100644 --- a/src/crypto/crypto_scrypt.cc +++ b/src/crypto/crypto_scrypt.cc @@ -38,7 +38,7 @@ ScryptConfig& ScryptConfig::operator=(ScryptConfig&& other) noexcept { } void ScryptConfig::MemoryInfo(MemoryTracker* tracker) const { - if (mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(mode)) { tracker->TrackFieldWithSize("pass", pass.size()); tracker->TrackFieldWithSize("salt", salt.size()); } @@ -72,13 +72,9 @@ Maybe ScryptTraits::AdditionalConfig( return Nothing(); } - params->pass = mode == kCryptoJobAsync - ? pass.ToCopy() - : pass.ToByteSource(); + params->pass = IsCryptoJobAsync(mode) ? pass.ToCopy() : pass.ToByteSource(); - params->salt = mode == kCryptoJobAsync - ? salt.ToCopy() - : salt.ToByteSource(); + params->salt = IsCryptoJobAsync(mode) ? salt.ToCopy() : salt.ToByteSource(); CHECK(args[offset + 2]->IsUint32()); // N CHECK(args[offset + 3]->IsUint32()); // r diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index a300c577ca10ae..851b715c67b495 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -3,6 +3,7 @@ #include "base_object-inl.h" #include "crypto/crypto_ec.h" #include "crypto/crypto_keys.h" +#include "crypto/crypto_pqc.h" #include "crypto/crypto_util.h" #include "env-inl.h" #include "memory_tracker-inl.h" @@ -237,34 +238,16 @@ bool UseP1363Encoding(const EVPKeyPointer& key, const DSASigEnc dsa_encoding) { } bool SupportsContextString(const EVPKeyPointer& key) { -#if OPENSSL_VERSION_NUMBER < 0x3020000fL - return false; -#else - switch (key.id()) { - case EVP_PKEY_ED25519: - case EVP_PKEY_ED448: + if (!OPENSSL_WITH_SIGNATURE_CONTEXT_STRING) return false; + + const int id = key.id(); #if OPENSSL_WITH_PQC - case EVP_PKEY_ML_DSA_44: - case EVP_PKEY_ML_DSA_65: - case EVP_PKEY_ML_DSA_87: - case EVP_PKEY_SLH_DSA_SHA2_128F: - case EVP_PKEY_SLH_DSA_SHA2_128S: - case EVP_PKEY_SLH_DSA_SHA2_192F: - case EVP_PKEY_SLH_DSA_SHA2_192S: - case EVP_PKEY_SLH_DSA_SHA2_256F: - case EVP_PKEY_SLH_DSA_SHA2_256S: - case EVP_PKEY_SLH_DSA_SHAKE_128F: - case EVP_PKEY_SLH_DSA_SHAKE_128S: - case EVP_PKEY_SLH_DSA_SHAKE_192F: - case EVP_PKEY_SLH_DSA_SHAKE_192S: - case EVP_PKEY_SLH_DSA_SHAKE_256F: - case EVP_PKEY_SLH_DSA_SHAKE_256S: + if (IsPqcSignatureKeyId(id)) return true; #endif - return true; - default: - return false; - } +#ifndef OPENSSL_IS_BORINGSSL + if (id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448) return true; #endif + return false; } } // namespace @@ -581,7 +564,7 @@ SignConfiguration& SignConfiguration::operator=( void SignConfiguration::MemoryInfo(MemoryTracker* tracker) const { tracker->TrackField("key", key); - if (job_mode == kCryptoJobAsync) { + if (IsCryptoJobAsync(job_mode)) { tracker->TrackFieldWithSize("data", data.size()); tracker->TrackFieldWithSize("signature", signature.size()); tracker->TrackFieldWithSize("context_string", context_string.size()); @@ -615,17 +598,15 @@ Maybe SignTraits::AdditionalConfig( params->key = std::move(data); } - ArrayBufferOrViewContents data(args[offset + 5]); + ArrayBufferOrViewContents data(args[offset + 6]); if (!data.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "data is too big"); return Nothing(); } - params->data = mode == kCryptoJobAsync - ? data.ToCopy() - : data.ToByteSource(); + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (args[offset + 6]->IsString()) { - Utf8Value digest(env->isolate(), args[offset + 6]); + if (args[offset + 7]->IsString()) { + Utf8Value digest(env->isolate(), args[offset + 7]); params->digest = Digest::FromName(*digest); if (!params->digest) [[unlikely]] { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", digest); @@ -633,39 +614,39 @@ Maybe SignTraits::AdditionalConfig( } } - if (args[offset + 7]->IsInt32()) { // Salt length + if (args[offset + 8]->IsInt32()) { // Salt length params->flags |= SignConfiguration::kHasSaltLength; params->salt_length = - GetSaltLenFromJS(args[offset + 7]).value_or(params->salt_length); + GetSaltLenFromJS(args[offset + 8]).value_or(params->salt_length); } - if (args[offset + 8]->IsUint32()) { // Padding + if (args[offset + 9]->IsUint32()) { // Padding params->flags |= SignConfiguration::kHasPadding; params->padding = - GetPaddingFromJS(params->key.GetAsymmetricKey(), args[offset + 8]); + GetPaddingFromJS(params->key.GetAsymmetricKey(), args[offset + 9]); } - if (args[offset + 9]->IsUint32()) { // DSA Encoding - params->dsa_encoding = GetDSASigEncFromJS(args[offset + 9]); + if (args[offset + 10]->IsUint32()) { // DSA Encoding + params->dsa_encoding = GetDSASigEncFromJS(args[offset + 10]); if (params->dsa_encoding == DSASigEnc::Invalid) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "invalid signature encoding"); return Nothing(); } } - if (!args[offset + 10]->IsUndefined()) { // Context string - ArrayBufferOrViewContents context_string(args[offset + 10]); + if (!args[offset + 11]->IsUndefined()) { // Context string + ArrayBufferOrViewContents context_string(args[offset + 11]); if (context_string.size() > 255) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "context string must be at most 255 bytes"); return Nothing(); } params->flags |= SignConfiguration::kHasContextString; - params->context_string = mode == kCryptoJobAsync + params->context_string = IsCryptoJobAsync(mode) ? context_string.ToCopy() : context_string.ToByteSource(); } if (params->mode == SignConfiguration::Mode::Verify) { - ArrayBufferOrViewContents signature(args[offset + 11]); + ArrayBufferOrViewContents signature(args[offset + 12]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -677,9 +658,8 @@ Maybe SignTraits::AdditionalConfig( if (UseP1363Encoding(akey, params->dsa_encoding)) { params->signature = ConvertSignatureToDER(akey, signature.ToByteSource()); } else { - params->signature = mode == kCryptoJobAsync - ? signature.ToCopy() - : signature.ToByteSource(); + params->signature = IsCryptoJobAsync(mode) ? signature.ToCopy() + : signature.ToByteSource(); } } diff --git a/src/crypto/crypto_turboshake.cc b/src/crypto/crypto_turboshake.cc new file mode 100644 index 00000000000000..3b4cb944bc63a7 --- /dev/null +++ b/src/crypto/crypto_turboshake.cc @@ -0,0 +1,652 @@ +#include "crypto/crypto_turboshake.h" +#include "async_wrap-inl.h" +#include "node_internals.h" +#include "threadpoolwork-inl.h" + +#include +#include + +namespace node::crypto { + +using v8::FunctionCallbackInfo; +using v8::JustVoid; +using v8::Local; +using v8::Maybe; +using v8::MaybeLocal; +using v8::Nothing; +using v8::Object; +using v8::Uint32; +using v8::Value; + +// ============================================================================ +// Keccak-p[1600, n_r=12] permutation +// Reference: FIPS 202, Section 3.3 and 3.4; RFC 9861 Section 2.2 +// Adapted from OpenSSL's keccak1600.c (KECCAK_REF variant) +// ============================================================================ +namespace { + +inline uint64_t ROL64(uint64_t val, int offset) { + DCHECK(offset >= 0 && offset < 64); + if (offset == 0) return val; + return (val << offset) | (val >> (64 - offset)); +} + +// Load/store 64-bit lanes in little-endian byte order. +// The Keccak state uses LE lane encoding (FIPS 202 Section 1, B.1). +// These helpers ensure correctness on both LE and BE platforms. +inline uint64_t LoadLE64(const uint8_t* src) { + return static_cast(src[0]) | (static_cast(src[1]) << 8) | + (static_cast(src[2]) << 16) | + (static_cast(src[3]) << 24) | + (static_cast(src[4]) << 32) | + (static_cast(src[5]) << 40) | + (static_cast(src[6]) << 48) | + (static_cast(src[7]) << 56); +} + +inline void StoreLE64(uint8_t* dst, uint64_t val) { + dst[0] = static_cast(val); + dst[1] = static_cast(val >> 8); + dst[2] = static_cast(val >> 16); + dst[3] = static_cast(val >> 24); + dst[4] = static_cast(val >> 32); + dst[5] = static_cast(val >> 40); + dst[6] = static_cast(val >> 48); + dst[7] = static_cast(val >> 56); +} + +static const unsigned char rhotates[5][5] = { + {0, 1, 62, 28, 27}, + {36, 44, 6, 55, 20}, + {3, 10, 43, 25, 39}, + {41, 45, 15, 21, 8}, + {18, 2, 61, 56, 14}, +}; + +// Round constants for Keccak-f[1600]. +// TurboSHAKE uses the last 12 rounds (indices 12..23). +static const uint64_t iotas[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, + 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, + 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, + 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, + 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL, +}; + +// Keccak-p[1600, 12]: the reduced-round permutation used by TurboSHAKE. +void KeccakP1600_12(uint64_t A[5][5]) { + for (size_t round = 12; round < 24; round++) { + // Theta + uint64_t C[5]; + for (size_t x = 0; x < 5; x++) { + C[x] = A[0][x] ^ A[1][x] ^ A[2][x] ^ A[3][x] ^ A[4][x]; + } + uint64_t D[5]; + for (size_t x = 0; x < 5; x++) { + D[x] = C[(x + 4) % 5] ^ ROL64(C[(x + 1) % 5], 1); + } + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] ^= D[x]; + } + } + + // Rho + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] = ROL64(A[y][x], rhotates[y][x]); + } + } + + // Pi + uint64_t T[5][5]; + memcpy(T, A, sizeof(T)); + for (size_t y = 0; y < 5; y++) { + for (size_t x = 0; x < 5; x++) { + A[y][x] = T[x][(3 * y + x) % 5]; + } + } + + // Chi + for (size_t y = 0; y < 5; y++) { + uint64_t row[5]; + for (size_t x = 0; x < 5; x++) { + row[x] = A[y][x] ^ (~A[y][(x + 1) % 5] & A[y][(x + 2) % 5]); + } + memcpy(A[y], row, sizeof(row)); + } + + // Iota + A[0][0] ^= iotas[round]; + } +} + +// ============================================================================ +// TurboSHAKE sponge construction +// RFC 9861 Section 2.2, Appendix A.2/A.3 +// ============================================================================ + +// TurboSHAKE128 rate = 168 bytes (1344 bits), capacity = 256 bits +// TurboSHAKE256 rate = 136 bytes (1088 bits), capacity = 512 bits +static constexpr size_t kTurboSHAKE128Rate = 168; +static constexpr size_t kTurboSHAKE256Rate = 136; + +void TurboSHAKE(const uint8_t* input, + size_t input_len, + size_t rate, + uint8_t domain_sep, + uint8_t* output, + size_t output_len) { + uint64_t A[5][5] = {}; + // Both rates (168, 136) are multiples of 8 + size_t lane_count = rate / 8; + + size_t offset = 0; + + // Absorb complete blocks from input + while (offset + rate <= input_len) { + for (size_t i = 0; i < lane_count; i++) { + A[i / 5][i % 5] ^= LoadLE64(input + offset + i * 8); + } + KeccakP1600_12(A); + offset += rate; + } + + // Absorb last (partial) block: remaining input bytes + domain_sep + padding + size_t remaining = input_len - offset; + uint8_t pad[168] = {}; // sized for max rate (TurboSHAKE128) + if (remaining > 0) { + memcpy(pad, input + offset, remaining); + } + pad[remaining] ^= domain_sep; + pad[rate - 1] ^= 0x80; + + for (size_t i = 0; i < lane_count; i++) { + A[i / 5][i % 5] ^= LoadLE64(pad + i * 8); + } + KeccakP1600_12(A); + + // Squeeze output + size_t out_offset = 0; + while (out_offset < output_len) { + size_t block = output_len - out_offset; + if (block > rate) block = rate; + size_t full_lanes = block / 8; + for (size_t i = 0; i < full_lanes; i++) { + StoreLE64(output + out_offset + i * 8, A[i / 5][i % 5]); + } + size_t rem = block % 8; + if (rem > 0) { + uint8_t tmp[8]; + StoreLE64(tmp, A[full_lanes / 5][full_lanes % 5]); + memcpy(output + out_offset + full_lanes * 8, tmp, rem); + } + out_offset += block; + if (out_offset < output_len) { + KeccakP1600_12(A); + } + } +} + +// Convenience wrappers +void TurboSHAKE128(const uint8_t* input, + size_t input_len, + uint8_t domain_sep, + uint8_t* output, + size_t output_len) { + TurboSHAKE( + input, input_len, kTurboSHAKE128Rate, domain_sep, output, output_len); +} + +void TurboSHAKE256(const uint8_t* input, + size_t input_len, + uint8_t domain_sep, + uint8_t* output, + size_t output_len) { + TurboSHAKE( + input, input_len, kTurboSHAKE256Rate, domain_sep, output, output_len); +} + +// ============================================================================ +// KangarooTwelve tree hashing (RFC 9861 Section 3) +// ============================================================================ + +static constexpr size_t kChunkSize = 8192; + +// length_encode(x): RFC 9861 Section 3.3 +// Returns byte string x_(n-1) || ... || x_0 || n +// where x = sum of 256^i * x_i, n is smallest such that x < 256^n +std::vector LengthEncode(size_t x) { + if (x == 0) { + return {0x00}; + } + + std::vector result; + size_t val = x; + while (val > 0) { + result.push_back(static_cast(val & 0xFF)); + val >>= 8; + } + + // Reverse to get big-endian: x_(n-1) || ... || x_0 + size_t n = result.size(); + for (size_t i = 0; i < n / 2; i++) { + std::swap(result[i], result[n - 1 - i]); + } + + // Append n (the length of the encoding) + result.push_back(static_cast(n)); + return result; +} + +using TurboSHAKEFn = void (*)(const uint8_t* input, + size_t input_len, + uint8_t domain_sep, + uint8_t* output, + size_t output_len); + +void KangarooTwelve(const uint8_t* message, + size_t msg_len, + const uint8_t* customization, + size_t custom_len, + uint8_t* output, + size_t output_len, + TurboSHAKEFn turboshake, + size_t cv_len) { + // Build S = M || C || length_encode(|C|) + auto len_enc = LengthEncode(custom_len); + size_t s_len = msg_len + custom_len + len_enc.size(); + + // Short message path: |S| <= 8192 + if (s_len <= kChunkSize) { + // Build S in a contiguous buffer + std::vector s(s_len); + size_t pos = 0; + if (msg_len > 0) { + memcpy(s.data() + pos, message, msg_len); + pos += msg_len; + } + if (custom_len > 0) { + memcpy(s.data() + pos, customization, custom_len); + pos += custom_len; + } + memcpy(s.data() + pos, len_enc.data(), len_enc.size()); + + turboshake(s.data(), s_len, 0x07, output, output_len); + return; + } + + // Long message path: tree hashing + // We need to process S in chunks, but S is virtual (M || C || length_encode) + // Build a helper to read from this virtual concatenation. + + // First chunk is S[0:8192], compute chaining values for rest + // FinalNode = S[0:8192] || 0x03 || 0x00^7 + + // We need to read from S = M || C || length_encode(|C|) + // Helper lambda to copy from virtual S + auto read_s = [&](size_t s_offset, uint8_t* buf, size_t len) { + size_t copied = 0; + // Part 1: message + if (s_offset < msg_len && copied < len) { + size_t avail = msg_len - s_offset; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, message + s_offset, to_copy); + copied += to_copy; + s_offset += to_copy; + } + // Part 2: customization + size_t custom_start = msg_len; + if (s_offset < custom_start + custom_len && copied < len) { + size_t off_in_custom = s_offset - custom_start; + size_t avail = custom_len - off_in_custom; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, customization + off_in_custom, to_copy); + copied += to_copy; + s_offset += to_copy; + } + // Part 3: length_encode + size_t le_start = msg_len + custom_len; + if (s_offset < le_start + len_enc.size() && copied < len) { + size_t off_in_le = s_offset - le_start; + size_t avail = len_enc.size() - off_in_le; + size_t to_copy = avail < (len - copied) ? avail : (len - copied); + memcpy(buf + copied, len_enc.data() + off_in_le, to_copy); + copied += to_copy; + } + }; + + // Start building FinalNode + // FinalNode = S_0 || 0x03 0x00^7 || CV_1 || CV_2 || ... || CV_(n-1) + // || length_encode(n-1) || 0xFF 0xFF + + // Read first chunk S_0 + std::vector first_chunk(kChunkSize); + read_s(0, first_chunk.data(), kChunkSize); + + // Start FinalNode with S_0 || 0x03 || 0x00^7 + std::vector final_node; + final_node.reserve(kChunkSize + 8 + ((s_len / kChunkSize) * cv_len) + 16); + final_node.insert(final_node.end(), first_chunk.begin(), first_chunk.end()); + final_node.push_back(0x03); + final_node.insert(final_node.end(), 7, 0x00); + + // Process remaining chunks + size_t offset = kChunkSize; + size_t num_blocks = 0; + std::vector chunk(kChunkSize); + std::vector cv(cv_len); + + while (offset < s_len) { + size_t block_size = s_len - offset; + if (block_size > kChunkSize) block_size = kChunkSize; + + chunk.resize(block_size); + read_s(offset, chunk.data(), block_size); + + // CV = TurboSHAKE(chunk, 0x0B, cv_len) + turboshake(chunk.data(), block_size, 0x0B, cv.data(), cv_len); + + final_node.insert(final_node.end(), cv.begin(), cv.end()); + num_blocks++; + offset += block_size; + } + + // Append length_encode(num_blocks) || 0xFF 0xFF + auto num_blocks_enc = LengthEncode(num_blocks); + final_node.insert( + final_node.end(), num_blocks_enc.begin(), num_blocks_enc.end()); + final_node.push_back(0xFF); + final_node.push_back(0xFF); + + // Final hash + turboshake(final_node.data(), final_node.size(), 0x06, output, output_len); +} + +void KT128(const uint8_t* message, + size_t msg_len, + const uint8_t* customization, + size_t custom_len, + uint8_t* output, + size_t output_len) { + KangarooTwelve(message, + msg_len, + customization, + custom_len, + output, + output_len, + TurboSHAKE128, + 32); +} + +void KT256(const uint8_t* message, + size_t msg_len, + const uint8_t* customization, + size_t custom_len, + uint8_t* output, + size_t output_len) { + KangarooTwelve(message, + msg_len, + customization, + custom_len, + output, + output_len, + TurboSHAKE256, + 64); +} + +} // anonymous namespace + +// ============================================================================ +// TurboShake bindings +// ============================================================================ + +TurboShakeConfig::TurboShakeConfig(TurboShakeConfig&& other) noexcept + : job_mode(other.job_mode), + variant(other.variant), + output_length(other.output_length), + domain_separation(other.domain_separation), + data(std::move(other.data)) {} + +TurboShakeConfig& TurboShakeConfig::operator=( + TurboShakeConfig&& other) noexcept { + if (&other == this) return *this; + this->~TurboShakeConfig(); + return *new (this) TurboShakeConfig(std::move(other)); +} + +void TurboShakeConfig::MemoryInfo(MemoryTracker* tracker) const { + if (IsCryptoJobAsync(job_mode)) { + // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource + tracker->TrackFieldWithSize("data", data.size()); + } +} + +Maybe TurboShakeTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + TurboShakeConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + // args[offset + 0] = algorithm name (string) + CHECK(args[offset]->IsString()); + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view alg = algorithm_name.ToStringView(); + + if (alg == "TurboSHAKE128") { + params->variant = TurboShakeVariant::TurboSHAKE128; + } else if (alg == "TurboSHAKE256") { + params->variant = TurboShakeVariant::TurboSHAKE256; + } else { + UNREACHABLE(); + } + + // args[offset + 1] = domain separation byte (uint32) + CHECK(args[offset + 1]->IsUint32()); + uint32_t domain_separation_u32 = args[offset + 1].As()->Value(); + CHECK_GE(domain_separation_u32, 0x01); + CHECK_LE(domain_separation_u32, 0x7F); + params->domain_separation = static_cast(domain_separation_u32); + + // args[offset + 2] = output length in bytes (uint32) + CHECK(args[offset + 2]->IsUint32()); + params->output_length = args[offset + 2].As()->Value(); + + // args[offset + 3] = data (ArrayBuffer/View) + ArrayBufferOrViewContents data(args[offset + 3]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); + + return JustVoid(); +} + +bool TurboShakeTraits::DeriveBits(Environment* env, + const TurboShakeConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CHECK_GT(params.output_length, 0); + char* buf = MallocOpenSSL(params.output_length); + + const uint8_t* input = reinterpret_cast(params.data.data()); + size_t input_len = params.data.size(); + + switch (params.variant) { + case TurboShakeVariant::TurboSHAKE128: + TurboSHAKE128(input, + input_len, + params.domain_separation, + reinterpret_cast(buf), + params.output_length); + break; + case TurboShakeVariant::TurboSHAKE256: + TurboSHAKE256(input, + input_len, + params.domain_separation, + reinterpret_cast(buf), + params.output_length); + break; + } + + *out = ByteSource::Allocated(buf, params.output_length); + return true; +} + +MaybeLocal TurboShakeTraits::EncodeOutput(Environment* env, + const TurboShakeConfig& params, + ByteSource* out) { + return out->ToArrayBuffer(env); +} + +// ============================================================================ +// KangarooTwelve bindings +// ============================================================================ + +KangarooTwelveConfig::KangarooTwelveConfig( + KangarooTwelveConfig&& other) noexcept + : job_mode(other.job_mode), + variant(other.variant), + output_length(other.output_length), + data(std::move(other.data)), + customization(std::move(other.customization)) {} + +KangarooTwelveConfig& KangarooTwelveConfig::operator=( + KangarooTwelveConfig&& other) noexcept { + if (&other == this) return *this; + this->~KangarooTwelveConfig(); + return *new (this) KangarooTwelveConfig(std::move(other)); +} + +void KangarooTwelveConfig::MemoryInfo(MemoryTracker* tracker) const { + if (IsCryptoJobAsync(job_mode)) { + // TODO(addaleax): Implement MemoryRetainer protocol for ByteSource + tracker->TrackFieldWithSize("data", data.size()); + tracker->TrackFieldWithSize("customization", customization.size()); + } +} + +Maybe KangarooTwelveTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + KangarooTwelveConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->job_mode = mode; + + // args[offset + 0] = algorithm name (string) + CHECK(args[offset]->IsString()); + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view alg = algorithm_name.ToStringView(); + + if (alg == "KT128") { + params->variant = KangarooTwelveVariant::KT128; + } else if (alg == "KT256") { + params->variant = KangarooTwelveVariant::KT256; + } else { + UNREACHABLE(); + } + + // args[offset + 1] = customization (BufferSource or undefined) + if (!args[offset + 1]->IsUndefined()) { + ArrayBufferOrViewContents customization(args[offset + 1]); + if (!customization.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); + return Nothing(); + } + params->customization = IsCryptoJobAsync(mode) + ? customization.ToCopy() + : customization.ToByteSource(); + } + + // args[offset + 2] = output length in bytes (uint32) + CHECK(args[offset + 2]->IsUint32()); + params->output_length = args[offset + 2].As()->Value(); + + // args[offset + 3] = data (ArrayBuffer/View) + ArrayBufferOrViewContents data(args[offset + 3]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->data = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); + + return JustVoid(); +} + +bool KangarooTwelveTraits::DeriveBits(Environment* env, + const KangarooTwelveConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CHECK_GT(params.output_length, 0); + + const uint8_t* input = reinterpret_cast(params.data.data()); + size_t input_len = params.data.size(); + + const uint8_t* custom = + reinterpret_cast(params.customization.data()); + size_t custom_len = params.customization.size(); + + // Guard against size_t overflow in KangarooTwelve's s_len computation: + // s_len = msg_len + custom_len + LengthEncode(custom_len).size() + // LengthEncode produces at most sizeof(size_t) + 1 bytes. + static constexpr size_t kMaxLengthEncodeSize = sizeof(size_t) + 1; + if (input_len > SIZE_MAX - custom_len || + input_len + custom_len > SIZE_MAX - kMaxLengthEncodeSize) { + return false; + } + + char* buf = MallocOpenSSL(params.output_length); + + switch (params.variant) { + case KangarooTwelveVariant::KT128: + KT128(input, + input_len, + custom, + custom_len, + reinterpret_cast(buf), + params.output_length); + break; + case KangarooTwelveVariant::KT256: + KT256(input, + input_len, + custom, + custom_len, + reinterpret_cast(buf), + params.output_length); + break; + } + + *out = ByteSource::Allocated(buf, params.output_length); + return true; +} + +MaybeLocal KangarooTwelveTraits::EncodeOutput( + Environment* env, const KangarooTwelveConfig& params, ByteSource* out) { + return out->ToArrayBuffer(env); +} + +// ============================================================================ +// Registration +// ============================================================================ + +void TurboShake::Initialize(Environment* env, Local target) { + TurboShakeJob::Initialize(env, target); + KangarooTwelveJob::Initialize(env, target); +} + +void TurboShake::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + TurboShakeJob::RegisterExternalReferences(registry); + KangarooTwelveJob::RegisterExternalReferences(registry); +} + +} // namespace node::crypto diff --git a/src/crypto/crypto_turboshake.h b/src/crypto/crypto_turboshake.h new file mode 100644 index 00000000000000..53b01eec8bd7c8 --- /dev/null +++ b/src/crypto/crypto_turboshake.h @@ -0,0 +1,105 @@ +#ifndef SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ +#define SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "crypto/crypto_util.h" + +namespace node::crypto { + +enum class TurboShakeVariant { TurboSHAKE128, TurboSHAKE256 }; + +struct TurboShakeConfig final : public MemoryRetainer { + CryptoJobMode job_mode; + TurboShakeVariant variant; + uint32_t output_length; // Output length in bytes + uint8_t domain_separation; // Domain separation byte (0x01–0x7F) + ByteSource data; + + TurboShakeConfig() = default; + + explicit TurboShakeConfig(TurboShakeConfig&& other) noexcept; + + TurboShakeConfig& operator=(TurboShakeConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(TurboShakeConfig) + SET_SELF_SIZE(TurboShakeConfig) +}; + +struct TurboShakeTraits final { + using AdditionalParameters = TurboShakeConfig; + static constexpr const char* JobName = "TurboShakeJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + TurboShakeConfig* params); + + static bool DeriveBits(Environment* env, + const TurboShakeConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const TurboShakeConfig& params, + ByteSource* out); +}; + +using TurboShakeJob = DeriveBitsJob; + +enum class KangarooTwelveVariant { KT128, KT256 }; + +struct KangarooTwelveConfig final : public MemoryRetainer { + CryptoJobMode job_mode; + KangarooTwelveVariant variant; + uint32_t output_length; // Output length in bytes + ByteSource data; + ByteSource customization; + + KangarooTwelveConfig() = default; + + explicit KangarooTwelveConfig(KangarooTwelveConfig&& other) noexcept; + + KangarooTwelveConfig& operator=(KangarooTwelveConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(KangarooTwelveConfig) + SET_SELF_SIZE(KangarooTwelveConfig) +}; + +struct KangarooTwelveTraits final { + using AdditionalParameters = KangarooTwelveConfig; + static constexpr const char* JobName = "KangarooTwelveJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_DERIVEBITSREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + KangarooTwelveConfig* params); + + static bool DeriveBits(Environment* env, + const KangarooTwelveConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput( + Environment* env, const KangarooTwelveConfig& params, ByteSource* out); +}; + +using KangarooTwelveJob = DeriveBitsJob; + +namespace TurboShake { +void Initialize(Environment* env, v8::Local target); +void RegisterExternalReferences(ExternalReferenceRegistry* registry); +} // namespace TurboShake + +} // namespace node::crypto + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS +#endif // SRC_CRYPTO_CRYPTO_TURBOSHAKE_H_ diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index 3435e43b3baa0c..e5c1c254179033 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -32,7 +32,9 @@ using ncrypto::DataPointer; using ncrypto::EnginePointer; #endif // !OPENSSL_NO_ENGINE using ncrypto::SSLPointer; +using v8::Array; using v8::ArrayBuffer; +using v8::ArrayBufferView; using v8::BackingStore; using v8::BackingStoreInitializationMode; using v8::BackingStoreOnFailureMode; @@ -40,6 +42,7 @@ using v8::BigInt; using v8::Context; using v8::EscapableHandleScope; using v8::Exception; +using v8::Function; using v8::FunctionCallbackInfo; using v8::HandleScope; using v8::Isolate; @@ -440,9 +443,9 @@ ByteSource ByteSource::FromBuffer(Local buffer, bool ntc) { ByteSource ByteSource::FromSecretKeyBytes( Environment* env, Local value) { - // A key can be passed as a string, buffer or KeyObject with type 'secret'. - // If it is a string, we need to convert it to a buffer. We are not doing that - // in JS to avoid creating an unprotected copy on the heap. + // JS normalizes secret KeyObject/CryptoKey inputs to a KeyObjectHandle. + // Strings are converted here instead of in JS to avoid creating an + // unprotected copy on the heap. return value->IsString() || IsAnyBufferSource(value) ? ByteSource::FromStringOrBuffer(env, value) : ByteSource::FromSymmetricKeyObjectHandle(value); @@ -514,44 +517,51 @@ Maybe Decorate(Environment* env, c = ToUpper(c); } -#define OSSL_ERROR_CODES_MAP(V) \ - V(SYS) \ - V(BN) \ - V(RSA) \ - V(DH) \ - V(EVP) \ - V(BUF) \ - V(OBJ) \ - V(PEM) \ - V(DSA) \ - V(X509) \ - V(ASN1) \ - V(CONF) \ - V(CRYPTO) \ - V(EC) \ - V(SSL) \ - V(BIO) \ - V(PKCS7) \ - V(X509V3) \ - V(PKCS12) \ - V(RAND) \ - V(DSO) \ - V(ENGINE) \ - V(OCSP) \ - V(UI) \ - V(COMP) \ - V(ECDSA) \ - V(ECDH) \ - V(OSSL_STORE) \ - V(FIPS) \ - V(CMS) \ - V(TS) \ - V(HMAC) \ - V(CT) \ - V(ASYNC) \ - V(KDF) \ - V(SM2) \ - V(USER) \ +#ifdef OPENSSL_IS_BORINGSSL +#define OSSL_ERROR_CODES_MAP_OPENSSL_ONLY(V) +#else +#define OSSL_ERROR_CODES_MAP_OPENSSL_ONLY(V) \ + V(PKCS12) \ + V(DSO) \ + V(OSSL_STORE) \ + V(FIPS) \ + V(TS) \ + V(CT) \ + V(ASYNC) \ + V(KDF) \ + V(SM2) +#endif + +#define OSSL_ERROR_CODES_MAP(V) \ + V(SYS) \ + V(BN) \ + V(RSA) \ + V(DH) \ + V(EVP) \ + V(BUF) \ + V(OBJ) \ + V(PEM) \ + V(DSA) \ + V(X509) \ + V(ASN1) \ + V(CONF) \ + V(CRYPTO) \ + V(EC) \ + V(SSL) \ + V(BIO) \ + V(PKCS7) \ + V(X509V3) \ + V(RAND) \ + V(ENGINE) \ + V(OCSP) \ + V(UI) \ + V(COMP) \ + V(ECDSA) \ + V(ECDH) \ + V(CMS) \ + V(HMAC) \ + V(USER) \ + OSSL_ERROR_CODES_MAP_OPENSSL_ONLY(V) #define V(name) case ERR_LIB_##name: lib = #name "_"; break; const char* lib = ""; @@ -561,6 +571,7 @@ Maybe Decorate(Environment* env, } #undef V #undef OSSL_ERROR_CODES_MAP +#undef OSSL_ERROR_CODES_MAP_OPENSSL_ONLY // Don't generate codes like "ERR_OSSL_SSL_". if (lib && strcmp(lib, "SSL_") == 0) prefix = ""; @@ -655,17 +666,72 @@ Maybe SetEncodedValue(Environment* env, if (!EncodeBignum(env, bn, size).ToLocal(&value)) { return Nothing(); } - return target->Set(env->context(), name, value).IsJust() ? JustVoid() - : Nothing(); + return target->DefineOwnProperty(env->context(), name, value).FromMaybe(false) + ? JustVoid() + : Nothing(); } CryptoJobMode GetCryptoJobMode(v8::Local args) { CHECK(args->IsUint32()); uint32_t mode = args.As()->Value(); - CHECK_LE(mode, kCryptoJobSync); + CHECK_LE(mode, kCryptoJobWebCrypto); return static_cast(mode); } +bool IsCryptoJobAsync(CryptoJobMode mode) { + return mode == kCryptoJobAsync || mode == kCryptoJobWebCrypto; +} + +MaybeLocal CreateWebCryptoJobError(Environment* env, + Local cause) { + Isolate* isolate = env->isolate(); + Local context = env->context(); + Local per_context_bindings; + Local domexception_ctor; + if (!GetPerContextExports(context).ToLocal(&per_context_bindings) || + !per_context_bindings + ->Get(context, FIXED_ONE_BYTE_STRING(isolate, "DOMException")) + .ToLocal(&domexception_ctor)) { + return {}; + } + CHECK(domexception_ctor->IsFunction()); + + Local options = Object::New(isolate); + if (options + ->Set(context, + FIXED_ONE_BYTE_STRING(isolate, "name"), + FIXED_ONE_BYTE_STRING(isolate, "OperationError")) + .IsNothing() || + options->Set(context, FIXED_ONE_BYTE_STRING(isolate, "cause"), cause) + .IsNothing()) { + return {}; + } + + Local argv[] = { + FIXED_ONE_BYTE_STRING(isolate, + "The operation failed for an operation-specific " + "reason"), + options, + }; + + return domexception_ctor.As()->NewInstance( + context, arraysize(argv), argv); +} + +MaybeLocal ToWebCryptoJobResult(Environment* env, Local value) { + if (value->IsArrayBuffer()) { + return value; + } + + if (Buffer::HasInstance(value)) { + return value.As()->Buffer(); + } + + CHECK(value->IsBoolean() || (value->IsObject() && !value->IsArray() && + !value->IsArrayBufferView())); + return value; +} + namespace { // SecureBuffer uses OpenSSL's secure heap feature to allocate a // Uint8Array. Without --secure-heap, OpenSSL's secure heap is disabled, @@ -689,7 +755,6 @@ void SecureBuffer(const FunctionCallbackInfo& args) { uint32_t len = args[0].As()->Value(); auto data = DataPointer::SecureAlloc(len); - CHECK(data.isSecure()); if (!data) { return THROW_ERR_OPERATION_FAILED(env, "Allocation failed"); } @@ -734,6 +799,7 @@ void Initialize(Environment* env, Local target) { NODE_DEFINE_CONSTANT(target, kCryptoJobAsync); NODE_DEFINE_CONSTANT(target, kCryptoJobSync); + NODE_DEFINE_CONSTANT(target, kCryptoJobWebCrypto); SetMethod(context, target, "secureBuffer", SecureBuffer); SetMethodNoSideEffect(context, target, "secureHeapUsed", SecureHeapUsed); diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 2e7a07fae9bc2e..1772a80dfa18d5 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -235,12 +235,16 @@ class ByteSource final { : data_(data), allocated_data_(allocated_data), size_(size) {} }; -enum CryptoJobMode { - kCryptoJobAsync, - kCryptoJobSync -}; +enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); +bool IsCryptoJobAsync(CryptoJobMode mode); + +v8::MaybeLocal CreateWebCryptoJobError(Environment* env, + v8::Local cause); + +v8::MaybeLocal ToWebCryptoJobResult(Environment* env, + v8::Local value); template class CryptoJob : public AsyncWrap, public ThreadPoolWork { @@ -269,9 +273,53 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { void AfterThreadPoolWork(int status) override { Environment* env = AsyncWrap::env(); - CHECK_EQ(mode_, kCryptoJobAsync); + CHECK(IsCryptoJobAsync(mode_)); CHECK(status == 0 || status == UV_ECANCELED); std::unique_ptr ptr(this); + if (mode_ == kCryptoJobWebCrypto) { + v8::HandleScope handle_scope(env->isolate()); + v8::Context::Scope context_scope(env->context()); + InternalCallbackScope callback_scope(this); + + if (status == UV_ECANCELED) { + v8::Local exception = v8::Exception::Error( + OneByteString(env->isolate(), "The operation was canceled")); + ptr->RejectWebCrypto(exception); + return; + } + + v8::Local err; + v8::Local result; + { + node::errors::TryCatchScope try_catch(env); + if (ptr->ToResult(&err, &result).IsNothing()) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + err = try_catch.Exception(); + } + } + + if (!err.IsEmpty() && !err->IsUndefined()) { + ptr->RejectWebCrypto(err); + return; + } + + CHECK(!result.IsEmpty()); + v8::Local webcrypto_result; + { + node::errors::TryCatchScope try_catch(env); + if (!ToWebCryptoJobResult(env, result).ToLocal(&webcrypto_result)) { + CHECK(try_catch.HasCaught()); + CHECK(try_catch.CanContinue()); + ptr->RejectWebCrypto(try_catch.Exception()); + return; + } + } + + ptr->ResolveWebCrypto(webcrypto_result); + return; + } + // If the job was canceled do not execute the callback. // TODO(@jasnell): We should likely revisit skipping the // callback on cancel as that could leave the JS in a pending @@ -326,6 +374,19 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { CryptoJob* job; ASSIGN_OR_RETURN_UNWRAP(&job, args.This()); + if (job->mode() == kCryptoJobWebCrypto) { + v8::Local resolver; + if (!v8::Promise::Resolver::New(env->context()).ToLocal(&resolver)) { + return; + } + + CHECK(job->resolver_.IsEmpty()); + job->resolver_.Reset(env->isolate(), resolver); + args.GetReturnValue().Set(resolver->GetPromise()); + + return job->ScheduleWork(); + } + if (job->mode() == kCryptoJobAsync) return job->ScheduleWork(); @@ -362,9 +423,80 @@ class CryptoJob : public AsyncWrap, public ThreadPoolWork { } private: + void ResolveWebCrypto(v8::Local value) { + Environment* env = AsyncWrap::env(); + v8::Local context = env->context(); + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + + bool should_delete_then = false; + v8::Local then_key; + v8::Local exception; + { + node::errors::TryCatchScope try_catch(env); + if (value->IsObject()) { + then_key = FIXED_ONE_BYTE_STRING(env->isolate(), "then"); + v8::Local object = value.As(); + v8::Maybe has_own_then = + object->HasOwnProperty(context, then_key); + if (has_own_then.IsNothing()) { + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } else if (!has_own_then.FromJust()) { + if (object + ->DefineOwnProperty(context, + then_key, + v8::Undefined(env->isolate()), + v8::DontEnum) + .FromMaybe(false)) { + should_delete_then = true; + } else if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } else { + exception = v8::Exception::Error(OneByteString( + env->isolate(), "Failed to prepare WebCrypto job result")); + } + } + } + + if (exception.IsEmpty() && resolver->Resolve(context, value).IsJust()) { + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } + resolver_.Reset(); + return; + } + if (try_catch.HasCaught() && try_catch.CanContinue()) { + exception = try_catch.Exception(); + } + } + + if (should_delete_then) { + USE(value.As()->Delete(context, then_key)); + } + if (!exception.IsEmpty()) { + USE(resolver->Reject(context, exception)); + } + resolver_.Reset(); + } + + void RejectWebCrypto(v8::Local cause) { + Environment* env = AsyncWrap::env(); + v8::Local exception; + if (!CreateWebCryptoJobError(env, cause).ToLocal(&exception)) { + exception = cause; + } + v8::Local resolver = + v8::Local::New(env->isolate(), resolver_); + USE(resolver->Reject(env->context(), exception)); + resolver_.Reset(); + } + const CryptoJobMode mode_; CryptoErrorStore errors_; AdditionalParams params_; + v8::Global resolver_; }; template @@ -399,17 +531,12 @@ class DeriveBitsJob final : public CryptoJob { CryptoJob::RegisterExternalReferences(New, registry); } - DeriveBitsJob( - Environment* env, - v8::Local object, - CryptoJobMode mode, - AdditionalParams&& params) + DeriveBitsJob(Environment* env, + v8::Local object, + CryptoJobMode mode, + AdditionalParams&& params) : CryptoJob( - env, - object, - DeriveBitsTraits::Provider, - mode, - std::move(params)) {} + env, object, DeriveBitsTraits::Provider, mode, std::move(params)) {} void DoThreadPoolWork() override { ncrypto::ClearErrorOnReturn clear_error_on_return; diff --git a/src/env_properties.h b/src/env_properties.h index 75b718e6171853..8ea21d1a36a2ec 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -395,7 +395,9 @@ V(contextify_global_template, v8::ObjectTemplate) \ V(contextify_wrapper_template, v8::ObjectTemplate) \ V(cpu_usage_template, v8::DictionaryTemplate) \ + V(crypto_cryptokey_constructor_template, v8::FunctionTemplate) \ V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \ + V(crypto_key_object_constructor_template, v8::FunctionTemplate) \ V(env_proxy_template, v8::ObjectTemplate) \ V(env_proxy_ctor_template, v8::FunctionTemplate) \ V(ephemeral_key_template, v8::DictionaryTemplate) \ @@ -467,6 +469,7 @@ V(async_hooks_init_function, v8::Function) \ V(async_hooks_promise_resolve_function, v8::Function) \ V(buffer_prototype_object, v8::Object) \ + V(crypto_internal_cryptokey_constructor, v8::Function) \ V(crypto_key_object_private_constructor, v8::Function) \ V(crypto_key_object_public_constructor, v8::Function) \ V(crypto_key_object_secret_constructor, v8::Function) \ diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 991cbf95fbb786..91d80e0dd379ba 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -48,6 +48,7 @@ namespace crypto { V(Hmac) \ V(Keygen) \ V(Keys) \ + V(NativeCryptoKey) \ V(NativeKeyObject) \ V(PBKDF2Job) \ V(Random) \ @@ -60,21 +61,26 @@ namespace crypto { V(Verify) \ V(X509Certificate) -#if !defined(OPENSSL_NO_ARGON2) && OPENSSL_VERSION_NUMBER >= 0x30200000L +#if OPENSSL_WITH_ARGON2 #define ARGON2_NAMESPACE_LIST(V) V(Argon2) #else #define ARGON2_NAMESPACE_LIST(V) -#endif // !OPENSSL_NO_ARGON2 && OpenSSL >= 3.2 +#endif // OPENSSL_WITH_ARGON2 -// KEM and KMAC functionality requires OpenSSL 3.0.0 or later -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #define KEM_NAMESPACE_LIST(V) V(KEM) -#define KMAC_NAMESPACE_LIST(V) V(Kmac) #else #define KEM_NAMESPACE_LIST(V) -#define KMAC_NAMESPACE_LIST(V) #endif +#if OPENSSL_WITH_KMAC +#define KMAC_NAMESPACE_LIST(V) V(Kmac) +#else +#define KMAC_NAMESPACE_LIST(V) +#endif // OPENSSL_WITH_KMAC + +#define TURBOSHAKE_NAMESPACE_LIST(V) V(TurboShake) + #ifdef OPENSSL_NO_SCRYPT #define SCRYPT_NAMESPACE_LIST(V) #else @@ -86,7 +92,8 @@ namespace crypto { ARGON2_NAMESPACE_LIST(V) \ KEM_NAMESPACE_LIST(V) \ KMAC_NAMESPACE_LIST(V) \ - SCRYPT_NAMESPACE_LIST(V) + SCRYPT_NAMESPACE_LIST(V) \ + TURBOSHAKE_NAMESPACE_LIST(V) void Initialize(Local target, Local unused, diff --git a/src/node_crypto.h b/src/node_crypto.h index e5e29544b57a81..ecc2b8c6a358c8 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -40,14 +40,16 @@ #include "crypto/crypto_hash.h" #include "crypto/crypto_hkdf.h" #include "crypto/crypto_hmac.h" -#if OPENSSL_VERSION_MAJOR >= 3 +#if OPENSSL_WITH_KEM #include "crypto/crypto_kem.h" +#endif +#if OPENSSL_WITH_KMAC #include "crypto/crypto_kmac.h" #endif #include "crypto/crypto_keygen.h" #include "crypto/crypto_keys.h" -#include "crypto/crypto_ml_dsa.h" #include "crypto/crypto_pbkdf2.h" +#include "crypto/crypto_pqc.h" #include "crypto/crypto_random.h" #include "crypto/crypto_rsa.h" #include "crypto/crypto_scrypt.h" @@ -55,6 +57,7 @@ #include "crypto/crypto_spkac.h" #include "crypto/crypto_timing.h" #include "crypto/crypto_tls.h" +#include "crypto/crypto_turboshake.h" #include "crypto/crypto_util.h" #include "crypto/crypto_x509.h" diff --git a/test/addons/openssl-get-ssl-ctx/binding.cc b/test/addons/openssl-get-ssl-ctx/binding.cc index 3945ec870fb8b9..47468ffbcd789d 100644 --- a/test/addons/openssl-get-ssl-ctx/binding.cc +++ b/test/addons/openssl-get-ssl-ctx/binding.cc @@ -18,12 +18,12 @@ void GetSSLCtx(const v8::FunctionCallbackInfo& args) { return; } - // Verify the pointer is a valid SSL_CTX by calling an OpenSSL function. - const SSL_METHOD* method = SSL_CTX_get_ssl_method(ctx); - if (method == nullptr) { + // Verify the pointer is a valid SSL_CTX by calling a function available + // across OpenSSL-compatible TLS backends and checking context-owned state. + STACK_OF(SSL_CIPHER)* ciphers = SSL_CTX_get_ciphers(ctx); + if (ciphers == nullptr) { isolate->ThrowException(v8::Exception::Error( - v8::String::NewFromUtf8(isolate, - "SSL_CTX_get_ssl_method returned nullptr") + v8::String::NewFromUtf8(isolate, "SSL_CTX_get_ciphers returned nullptr") .ToLocalChecked())); return; } diff --git a/test/common/boringssl.js b/test/common/boringssl.js new file mode 100644 index 00000000000000..e6e91387c304c7 --- /dev/null +++ b/test/common/boringssl.js @@ -0,0 +1,346 @@ +/* eslint-disable node-core/crypto-check */ + +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fixtures = require('../common/fixtures'); +const tls = require('tls'); + +// This module is for BoringSSL-specific branches in tests whose original +// OpenSSL coverage cannot run unchanged. Each helper should assert the +// observable BoringSSL behavior that explains why the OpenSSL-specific +// assertions are bypassed. + +/** + * BoringSSL exposes many removed or disabled TLS cipher suites as "no match" + * at secure-context creation time. This is used for suites such as + * finite-field DHE and anonymous ECDH that OpenSSL builds may still negotiate + * in tests. + * @param {Function} fn + */ +function assertNoCipherMatch(fn) { + assert.throws(fn, { + code: 'ERR_SSL_NO_CIPHER_MATCH', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'NO_CIPHER_MATCH', + }); +} + +/** + * BoringSSL does not parse OpenSSL cipher-string commands such as `@SECLEVEL`. + * Those are OpenSSL policy directives, not cipher names. + * @param {Function} fn + */ +function assertInvalidCommand(fn) { + assert.throws(fn, { + code: 'ERR_SSL_INVALID_COMMAND', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'INVALID_COMMAND', + }); +} + +/** + * Node's DHE tests exercise OpenSSL's finite-field DHE cipher support and DH + * parameter-size policy. BoringSSL does not offer these DHE cipher suites on + * this surface, so creating a server context with a DHE-only cipher list fails + * before a handshake can test DH parameter behavior. + */ +function assertFiniteFieldDheUnsupported() { + assertNoCipherMatch(() => { + tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'DHE-RSA-AES128-GCM-SHA256', + }); + }); +} + +/** + * OpenSSL security levels reject small keys by policy and can be adjusted with + * `@SECLEVEL` in the cipher string. BoringSSL does not implement those security + * levels: the small-key server context is accepted, while the OpenSSL-specific + * `@SECLEVEL` command is rejected as invalid cipher-string syntax. + */ +function assertOpenSSLSecurityLevelsUnsupported() { + const options = { + key: fixtures.readKey('agent11-key.pem'), + cert: fixtures.readKey('agent11-cert.pem'), + ciphers: 'DEFAULT', + }; + + tls.createServer(options).close(); + + options.ciphers = 'DEFAULT:@SECLEVEL=0'; + assertInvalidCommand(() => tls.createServer(options)); +} + +/** + * Node's multi-key tests rely on OpenSSL accepting an array of private keys and + * matching them with an array of certificates. BoringSSL rejects this mixed + * EC/RSA identity configuration while configuring the certificate chain, before + * a client can negotiate either identity. + */ +function assertMultiKeyUnsupported() { + assert.throws(() => { + tls.createServer({ + key: [ + fixtures.readKey('ec10-key.pem'), + fixtures.readKey('agent1-key.pem'), + ], + cert: [ + fixtures.readKey('agent1-cert.pem'), + fixtures.readKey('ec10-cert.pem'), + ], + }); + }, { + code: 'ERR_OSSL_X509_KEY_TYPE_MISMATCH', + library: 'X.509 certificate routines', + function: 'OPENSSL_internal', + reason: 'KEY_TYPE_MISMATCH', + }); +} + +/** + * BoringSSL does not support caller-initiated renegotiation. Even on a TLS 1.2 + * connection, TLSSocket#renegotiate() returns false and the callback receives + * Node's BoringSSL-specific unsupported-renegotiation error instead of + * entering the native binding or exercising Node's renegotiation-limit logic. + */ +function testRenegotiationUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('rsa_private.pem'), + cert: fixtures.readKey('rsa_cert.crt'), + maxVersion: 'TLSv1.2', + }, (socket) => socket.resume()); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(() => { + const ok = client.renegotiate({}, common.mustCall((err) => { + assert.throws(() => { throw err; }, { + code: 'ERR_TLS_RENEGOTIATION_UNSUPPORTED', + message: 'TLS session renegotiation is unsupported by this TLS ' + + 'implementation', + }); + client.destroy(); + server.close(); + })); + assert.strictEqual(ok, false); + })); + client.on('error', common.mustNotCall()); + })); +} + +/** + * OpenSSL exposes the negotiated ephemeral key type, name, and size for TLS + * clients. With BoringSSL the same ECDHE TLS 1.2 handshake succeeds, but + * getEphemeralKeyInfo() returns null on the server side and an object whose + * fields are undefined on the client side. + */ +function testEphemeralKeyInfoUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + ciphers: 'ECDHE-RSA-AES256-GCM-SHA384', + ecdhCurve: 'prime256v1', + maxVersion: 'TLSv1.2', + }, common.mustCall((socket) => { + assert.strictEqual(socket.getEphemeralKeyInfo(), null); + socket.end(); + })); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(() => { + assert.deepStrictEqual(client.getEphemeralKeyInfo(), { + type: undefined, + name: undefined, + size: undefined, + }); + server.close(); + })); + })); +} + +/** + * The protocol matrix tests cover OpenSSL behavior for legacy TLS protocols. + * For BoringSSL we only need to exhibit that a TLSv1-only client cannot connect + * to a server whose minimum protocol is TLS 1.2; the client receives the + * protocol-version alert instead of the OpenSSL version-specific matrix. + */ +function testLegacyProtocolUnsupported() { + const server = tls.createServer({ + key: fixtures.readKey('agent2-key.pem'), + cert: fixtures.readKey('agent2-cert.pem'), + minVersion: 'TLSv1.2', + }, common.mustNotCall()); + + server.on('tlsClientError', common.mustCall()); + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + secureProtocol: 'TLSv1_method', + }, common.mustNotCall()); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + server.close(); + })); + })); +} + +/** + * BoringSSL can load a multi-PFX option well enough to serve the ECDSA + * identity, but it does not provide the same OpenSSL multi-identity selection + * behavior. After the ECDSA handshake succeeds, an RSA-only client fails with + * no shared cipher instead of selecting the RSA identity from the same PFX list. + */ +function testMultiPfxSelectionDifference() { + const server = tls.createServer({ + pfx: [ + { + buf: fixtures.readKey('agent1.pfx'), + passphrase: 'sample', + }, + fixtures.readKey('ec.pfx'), + ], + }, common.mustCallAtLeast((socket) => socket.end(), 1)); + + server.listen(0, common.mustCall(() => { + const ecdsa = tls.connect(server.address().port, { + ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384', + maxVersion: 'TLSv1.2', + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(ecdsa.getCipher().name, + 'ECDHE-ECDSA-AES256-GCM-SHA384'); + ecdsa.end(); + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_NO_SHARED_CIPHER'); + })); + const rsa = tls.connect(server.address().port, { + ciphers: 'ECDHE-RSA-AES256-GCM-SHA384', + maxVersion: 'TLSv1.2', + rejectUnauthorized: false, + }, common.mustNotCall()); + rsa.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'); + server.close(); + })); + })); + })); +} + +/** + * PSK works for TLS 1.2 in BoringSSL, but Node's PSK tests also cover the + * default TLS 1.3 path. In that path BoringSSL does not complete a certificate- + * less PSK-only handshake through Node's current server setup: the server + * reports NO_CERTIFICATE_SET and the client receives an internal-error alert. + */ +function testPskTls13Unsupported() { + const key = Buffer.from('d731ef57be09e5204f0b205b60627028', 'hex'); + let gotClientError = false; + let gotServerError = false; + function maybeClose(server) { + if (gotClientError && gotServerError) + server.close(); + } + + const server = tls.createServer({ + ciphers: 'PSK+HIGH', + pskCallback() { return key; }, + }, common.mustNotCall()); + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_NO_CERTIFICATE_SET'); + gotServerError = true; + maybeClose(server); + })); + + server.listen(0, common.mustCall(() => { + const client = tls.connect({ + port: server.address().port, + ciphers: 'PSK+HIGH', + checkServerIdentity() {}, + pskCallback() { + return { psk: key, identity: 'TestUser' }; + }, + }, common.mustNotCall()); + client.on('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR'); + gotClientError = true; + maybeClose(server); + })); + })); +} + +/** + * The OpenSSL ticket tests assume that once a TLS 1.3 session is reused, the + * client will not necessarily receive a replacement session event before close. + * BoringSSL emits new session tickets on both the initial and resumed TLS 1.3 + * connections, so the resumed connection still emits at least one 'session' + * event while isSessionReused() is true. + */ +function testTls13SessionTicketSemanticsDiffer() { + const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + }, (socket) => socket.end()); + + let session; + let secondSessionEvents = 0; + + server.listen(0, common.mustCall(() => { + const first = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + }, common.mustCall(() => { + assert.strictEqual(first.isSessionReused(), false); + })); + first.on('session', common.mustCallAtLeast((sess) => { + session = sess; + }, 1)); + first.on('close', common.mustCall(() => { + assert(Buffer.isBuffer(session)); + + const second = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + session, + }, common.mustCall(() => { + assert.strictEqual(second.isSessionReused(), true); + })); + second.on('session', common.mustCallAtLeast(() => { + secondSessionEvents++; + }, 1)); + second.on('close', common.mustCall(() => { + assert(secondSessionEvents > 0); + server.close(); + })); + second.resume(); + })); + first.resume(); + })); +} + +module.exports = { + assertFiniteFieldDheUnsupported, + assertMultiKeyUnsupported, + assertNoCipherMatch, + assertOpenSSLSecurityLevelsUnsupported, + testEphemeralKeyInfoUnsupported, + testLegacyProtocolUnsupported, + testMultiPfxSelectionDifference, + testPskTls13Unsupported, + testRenegotiationUnsupported, + testTls13SessionTicketSemanticsDiffer, +}; diff --git a/test/fixtures/keys/Makefile b/test/fixtures/keys/Makefile index b78bea659628c5..d5bdd8f46ff573 100644 --- a/test/fixtures/keys/Makefile +++ b/test/fixtures/keys/Makefile @@ -101,6 +101,8 @@ all: \ ml_dsa_44_private.pem \ ml_dsa_44_private_seed_only.pem \ ml_dsa_44_private_priv_only.pem \ + ml_dsa_44_private_encrypted.pem \ + ml_dsa_44_private_encrypted.der \ ml_dsa_44_public.pem \ ml_dsa_65_private.pem \ ml_dsa_65_private_seed_only.pem \ @@ -110,6 +112,12 @@ all: \ ml_dsa_87_private_seed_only.pem \ ml_dsa_87_private_priv_only.pem \ ml_dsa_87_public.pem \ + ml-dsa-44.json \ + ml-dsa-65.json \ + ml-dsa-87.json \ + ml-kem-512.json \ + ml-kem-768.json \ + ml-kem-1024.json \ ml_kem_512_private.pem \ ml_kem_512_private_seed_only.pem \ ml_kem_512_private_priv_only.pem \ @@ -117,6 +125,8 @@ all: \ ml_kem_768_private.pem \ ml_kem_768_private_seed_only.pem \ ml_kem_768_private_priv_only.pem \ + ml_kem_768_private_encrypted.pem \ + ml_kem_768_private_encrypted.der \ ml_kem_768_public.pem \ ml_kem_1024_private.pem \ ml_kem_1024_private_seed_only.pem \ @@ -146,6 +156,18 @@ all: \ slh_dsa_shake_256s_public.pem \ slh_dsa_shake_256f_private.pem \ slh_dsa_shake_256f_public.pem \ + slh-dsa-sha2-128s.json \ + slh-dsa-sha2-128f.json \ + slh-dsa-sha2-192s.json \ + slh-dsa-sha2-192f.json \ + slh-dsa-sha2-256s.json \ + slh-dsa-sha2-256f.json \ + slh-dsa-shake-128s.json \ + slh-dsa-shake-128f.json \ + slh-dsa-shake-192s.json \ + slh-dsa-shake-192f.json \ + slh-dsa-shake-256s.json \ + slh-dsa-shake-256f.json \ # # Create Certificate Authority: ca1 @@ -987,6 +1009,12 @@ ml_dsa_44_private_priv_only.pem: ml_dsa_44_private.pem ml_dsa_44_public.pem: ml_dsa_44_private.pem openssl pkey -in ml_dsa_44_private.pem -pubout -out ml_dsa_44_public.pem +ml_dsa_44_private_encrypted.pem: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -out ml_dsa_44_private_encrypted.pem + +ml_dsa_44_private_encrypted.der: ml_dsa_44_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-dsa.output_formats=seed-only -in ml_dsa_44_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_dsa_44_private_encrypted.der + ml_dsa_65_private.pem: openssl genpkey -algorithm ml-dsa-65 -out ml_dsa_65_private.pem @@ -1035,6 +1063,12 @@ ml_kem_768_private_priv_only.pem: ml_kem_768_private.pem ml_kem_768_public.pem: ml_kem_768_private.pem openssl pkey -in ml_kem_768_private.pem -pubout -out ml_kem_768_public.pem +ml_kem_768_private_encrypted.pem: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -out ml_kem_768_private_encrypted.pem + +ml_kem_768_private_encrypted.der: ml_kem_768_private_seed_only.pem + openssl pkcs8 -topk8 -v2 aes-256-cbc -provparam ml-kem.output_formats=seed-only -in ml_kem_768_private_seed_only.pem -passout 'pass:password' -outform DER -out ml_kem_768_private_encrypted.der + ml_kem_1024_private.pem: openssl genpkey -algorithm ml-kem-1024 -out ml_kem_1024_private.pem @@ -1047,6 +1081,39 @@ ml_kem_1024_private_priv_only.pem: ml_kem_1024_private.pem ml_kem_1024_public.pem: ml_kem_1024_private.pem openssl pkey -in ml_kem_1024_private.pem -pubout -out ml_kem_1024_public.pem +define GEN_PQC_JWK +$(1): $(subst -,_,$(basename $(1)))_private.pem + ../../../node -e '\ + const { readFileSync, writeFileSync } = require("fs"); \ + const { createPrivateKey } = require("crypto"); \ + const pem = "$(subst -,_,$(basename $(1)))_private.pem"; \ + const key = createPrivateKey(readFileSync(pem)); \ + const jwk = JSON.stringify(key.export({ format: "jwk" }), null, 2); \ + writeFileSync("$(1)", jwk + "\n")' +endef + +PQC_JWK_FIXTURES = \ + ml-dsa-44.json \ + ml-dsa-65.json \ + ml-dsa-87.json \ + ml-kem-512.json \ + ml-kem-768.json \ + ml-kem-1024.json \ + slh-dsa-sha2-128s.json \ + slh-dsa-sha2-128f.json \ + slh-dsa-sha2-192s.json \ + slh-dsa-sha2-192f.json \ + slh-dsa-sha2-256s.json \ + slh-dsa-sha2-256f.json \ + slh-dsa-shake-128s.json \ + slh-dsa-shake-128f.json \ + slh-dsa-shake-192s.json \ + slh-dsa-shake-192f.json \ + slh-dsa-shake-256s.json \ + slh-dsa-shake-256f.json \ + +$(foreach v,$(PQC_JWK_FIXTURES),$(eval $(call GEN_PQC_JWK,$(v)))) + x448_private.pem: openssl genpkey -algorithm x448 -out x448_private.pem diff --git a/test/fixtures/keys/ml-dsa-44.json b/test/fixtures/keys/ml-dsa-44.json new file mode 100644 index 00000000000000..9c5005576ac51a --- /dev/null +++ b/test/fixtures/keys/ml-dsa-44.json @@ -0,0 +1,6 @@ +{ + "priv": "273AhMPiZWLlSQCY41yi1fMj6xavGH0btB23zMhI1uY", + "kty": "AKP", + "alg": "ML-DSA-44", + "pub": "fYmD1Rx_jkoW9KG7Bs_5zyYEiWEZs15tYBxNdKq9NircZnvZBwwwaGbj0UsxJNc4Dyfp2IFAZZPO3rFCSUdpXHPrGRHwIVMzwiwfu2V7V02xoheW4mrkPThA3JRJSmNdsx6YGu37MaeJkIk6AlUexo46JfGrkRXZp_IyZxiL_L2dPrfwx-32j7WFI5sBadp7cDWfNkJjdQwW4puTe5Rw7h16GHb-DMOAKpfeMHujh7IYHuLCU6lVi90j1m8Ru0dxdmeQ1eY1vDnO7fNQKfzOLhpUNnj7BBZ24GTqFc-SN5HDCSCsSGKScTYYBwiSVTdSGG1GNqIiN2FgE4z1Jj6JFVB_OIUnl4sKbb3m8kB0BwtUPbkC0FVokGRUEGt6ba1Pc_IMpB5Gs3g9PFREI_C9o1yVW3NS2PzH_Vk4Tpf0N1K1kzIK_3IqekLfyqXmVDNsOovsS7Sw9TdmdWUNGRmhXFKRkex5VjpMIx7OwBGsYJCc4FhauWdrVtbkvHGggSpsla73ZcA4Vzh7aq47LMv0KS2YLp-DMn7SEohPHGg74118eLLn88yptxwtwt1dBFj8BKUfPrytuN1EIRQy34hwbkBLN9wDqhgn3Z3fvksRvmgN_4ZQ8YjeD-H3OFh5WJ_Rd66wHSl-YFat-_JF4UPcdlkNUbxPvDi5VL909Pe3VlwEZhT5otdtXQX4U3dUfqWKEh2kN0Q2lo8wbf3OMmBOFTfyX0eYa_5088ZnJvvliefn-TCDyc6WlcZrNqwBOF8N8-IN3b_8RPq-RuV8-mK-M83Hi4ElQB7Z44eZMmfUwFrozEG4Wq2K6MwQ_edG4dWeUVMCloTpGDFOtlLQlDoAN4m_sS2Lbwm_3ra29noUcK8_j10yy-hENE2Yluh1pIL-GoWZj3uYO-rEKVbszaagdE0DJ_uQcHUdNnBHKn64-cQ6xihXzxaeHx9OxkWWMKbzLtKpuYDK_X7EVvm8YTjl_oTsr2SWT2usjNJko32DhRV-OXLKKHo5FJpCy2bGFLXGG26CglUvgZQ2dyXiWeGVNKffOv1cQ5R_RlU2MpLiZ1bigy9hh4lu_XAHLfjQfhf71jeMuF4nEBWV-YOAjDTaDB2hcGqv_XcGXcmLWHqOWgc5Mb6lkb2zYs_oyOskmyFx6C0P7UrV8kCiN4zbuTqZNdNjlWL_QJUmU3vk6CpNa0XN1M3sLjZpOEsaqgRVPLcIDH-juVhyWiymuxe-8yNCOFSKxhscew08EQ9DEckP_iIA8qU2gcreHtvAS5VA5Emz1K2ypYe6oS3ogP-CX4nOAEfvjsb1HHJoclgiwjL1BtCLFgOE-0vn1M-nVOE6WbHGHoNKMJMHP2a3HQC7DmDfSOw5P6Cj5X7QVqhCY6tAGZWEPu3hUssp7K5UJePEdBn_LrErt4ucyXW6y1PAA2Fn8EuHaRyf2ggibDGnzq8E15m_R4LMvZAuGR0bN9jBTlm_x4ZQMqFwKkIdllkN1QTErazOyNsgU6fhA_20h5EIYT6-LqXr_Otj3Kp8MkJB9c3XNGoo5sbHTQCt0VNOHoxCFP_swiAJLtm743eOsI1M6naWLIqPagSCioosAvJYowypJQGvM-N3hBu8KUr0f911KRN7WqTAXTOHZ_vvTqcWKet0dFdh1EHuP3TrU8hSMciaphGvuK93T3gaWuJ6lcCkQndWvEo9S6FQB7eLU_ALKOQ3ROybUUkXgfyTkWDPxbHdeJCgMRv6Ig1PShPyxYb4ig" +} diff --git a/test/fixtures/keys/ml-dsa-65.json b/test/fixtures/keys/ml-dsa-65.json new file mode 100644 index 00000000000000..0f11b2935daffc --- /dev/null +++ b/test/fixtures/keys/ml-dsa-65.json @@ -0,0 +1,6 @@ +{ + "priv": "1X9VEr_iXMRwBvnSytEmHrtA-DpD6FWAUqMrDNlJVBg", + "kty": "AKP", + "alg": "ML-DSA-65", + "pub": "hxPP5LvG83t2fJyfA1TUssJK_ydrzryrCHGZuKFxmnl5Y3sxHRCPW_JpHEoiIgR6kgELnwibZnueax1zFerTOTA7o0NwXHFiaEB-8AmqJI93DkvtbUOSTCixa3admQBKW_PtgMCVtaEVuuvCuOEFhOyuZkyfvnpBwUKOkz3t-O1wpgrSmf-rdPXOEv8YcsSn-xfLYPSLzPCnt7gnIX_fwtkgnXjref-QqjFKlKZE2e7MkmHeViJ4iGy78r3UzVhBHsmFGC0ZNc8-iT3muH5Sn0SXmNq-F2EoerWLIAsPxL2KE6UrqPAwTbHn1B5sAGWvhsVVLlFPI1s1JLVLBNRJ5vhif525xNIpMAMuAZrteD827pve3zQo9_GHjWgykj9VzM9PEcVmVqxZ5u41kUXsM4PWZF29Oh2sYsmJ2LdiJ9RcA91vRLG2DqEYm-V5JwIz8uxL17DUsEC7zYthvtqGASq05CbfPTBev33rQUv4H0Etz99U89WooTk0FisHDz1uEUilU_VY1tN5byIDitXNf0jnz3SIHDUUZARn7ll0YwO0jtksT68sQW3Liy6Exhlp1td0so2qZUrbVZasjyCOVuibwbwvrdpP3QRsoG5UqkAqk8Rm2iCpdQSg87pswOscgA8AC8TczGHNfXc9PqzAmbsEPKvmZuE60HLGzqpRqFULf3nyYUQUqbdmKJsKQ29LXeDVbyy3-fkTUDuYqNC2tBY7PkzHJSA9Z4hDC_BHEFxcelibScSNyf7y4lDVWnuJMXpQ0WRh3UkUPa007IerhixwxvBvFXQR-ytYinixvjirlcEF1wQI1DzE8KjOXYYuFPS4Yl8HeZHQ-64Q0RuxlKIRP3YvjZWh4IDVvEVs5ZLzZPbE3Twe0N5a7iCu0BzZWTeHNbcMoViFyJTpec2w3vVHeI4PJB-5HeI8xuh-9y8ytTau8QtMe4thoROoajizDQLrkw3e6ryJJ3R84i0oni4vmZWyLDilwcLqPOkQJCIDMjq7exdmVX5t3DtAW4F6Coz0z3sf7tGlSMVxA7izCoVbG0y2_l1P2h7fWBuEPT7PWlMdPqu9Pj5jqXY6jJ0nkaR_pp7dDhO1HKae5edcBYunHZqVQQjRZ_DvKzbPrDk5t6Xq9fdSkiAeP3B4qn5uU-nx7OaX7DRoVEnbbiEDynIRPSEY-Ts3alPJtBv8zuzaGNyX05Z9MyZ0w-VlC-WxOBdVEsIAp_4uJ3kQ3UsfE9DLJH8WPuDI4t4i2VnNNyFlI0XSUocc_0rWgqp2I1UzSzkVbklwkuFywPI645u4G2XAlfdd_wpjFGC-IUPXgpeSfspPwW15sBP-ITS-gwtvfzQVLpRS0euzN97xo_GMhNPZ4bW-YyZt8z_R8bsQ8ktfoP-5RUV-yzYDt0tA01QJsZdBLf5J_H7qP8l4c8V4hPe_CFL032obbxmAnVPAP69u2SaMBlL8azjk4wGVFQpQp1JqMJCao2W8ZImCVegkPZxhGbx0nkgVfyFx4ihMeDNM288JbGC4CGON8C02Q84rQzhwzZE83Y9rSe1Bb0fUMHMu6ihD5jLdeltuBL4ZdJlKgL24KZK5o5pq4_l9SyzGAjB1KAQnClNOB1SxV89CtILu-65wb17s0z3qw2-NF0B6UVlGQFebjbSyLQv2ARaETh_8cBiPugVMgBIV3K1KBwNyWejyI1ZDCssvIZHJCF2SRW3HmJerTiB23eGHFYKSLdxW7LEzoHIc2xZEc3pwR43gavjeoL0pNc-HNFV_c19wiH7Tnw3IHld_FfTqAIPnqKMNIY7D_D0DmFNTOdnzcipqKxUB0Avc-wr8Fz0gjeRpLH2iDSCJWtvWjoeYvHTktGsblDAM5j9xznwEvZfQvj8fTUnFxl3clkD6e9V1jrDQDkXfOtl-bDIv9PtMwamfJFu-z2ubF-gKytUewPNo10uhwr2TDNdUayCZDR2T3HoRLN8goIw2bFoPJ98LoPcSukEvKABjH0DiHNeqFELNZPx_uCx5N-YFkUZxHWA1QUoGhqQ3REtcT3c-SZf_TDFOPvws6bmwt4lcWpLmubOAJLFt6J8m8HCkVUshRdFzvHQm_0JEvA3JtyXZzvsPUv5njdk0nxTZktvsnqX054RQk5x8U-lBY-bK3uMOoFnHju45LMoHCUgGJi22eUm7nLGZEh84ZAbNlPLXpfavXvPJh21OW5EOAeuQ-yWNHY2xbmAiHNnb-J2VpZc1Vy82sxn8umFtKduuQuIQMOsf4qHqj5MzDY_1NjrM4Wm7XAiLC4MpQ22w9PWNQXSZWvo2fj8WUnfEibpgyRkoD2P25GRQqsRJ3-Ykl5bm_2Vfe6i3oXHOwQwZwKGXfAqXyo4iU1UI7e-qC4sj5U64oB_A_NSBaJJrZoQ2fVeGTnFxA4QMMoWCT0VlwBXK0B3jht8Xal3WcI-i9ctQB1-GrmwwgG2ttePHt1IKy69bSZE3FLkFicaHg6VxypG6ef8rVsmMrfpTATOnF5_iEaLNY9428HHGW0iz4vXwaE-MkYy7NK2KMPFiCB0ec9OjIROwayK4LREv4qknWHnVQRSm25Rr9DcVFXKj16Au7X1hv7TuVH7h25U" +} diff --git a/test/fixtures/keys/ml-dsa-87.json b/test/fixtures/keys/ml-dsa-87.json new file mode 100644 index 00000000000000..401b21fc8702bf --- /dev/null +++ b/test/fixtures/keys/ml-dsa-87.json @@ -0,0 +1,6 @@ +{ + "priv": "LZSOlEPbU9S5_mSsMULffTyxZu6qKEOQ1nfEi2NCscg", + "kty": "AKP", + "alg": "ML-DSA-87", + "pub": "DZXqaBATRN0GRtigxzkLxp7C9fFYxI7Gl-tdfXqJHbzVCTTvRfRwZcu3YmpsUYXBdX2pVsQ51QlqxMslKBRmfNCanBLcfd57qoEIb0K6GIKZGHxlsr9aXNjEGcKMo0ICon0LYTvTWrl72Oz-2yEA_abPK3_dBUFGAYQ6kOQhAHcT1CMmTTck23PnEd5WUpYfZOA9giFX9dNVrrdFWczj_vDOty81ObNKsxfVWT1nG7c60UCJxb2c2tMrBx3rp7Hfc_aOg54W5KHocJi0Eai0ok4buySTe0UCSCUTkeoCcdiABgOFBiXRYzpm3Lz4uot6hSgFpuh67fE9Zpgtn64vfI-O1mgcPrPPpd3yA92Jrq-dvXuM55w1RmA_hha3U5Sh2vm0tD1U57q945UppFReIv_8NAKBkxQ_vHil7ySm-m7IAM-sTUY86_IqMZqisxoz7Ff7ZR2vIiUm3-L0ow4B8uPsCv2ZlUoVXvMF6XQiOHsgqgP1rfH8DmfmPFudwiXrAW6wEmi10skPmkN92aC3TPG6nmaNryQ7f8J82yVmGxW9U7zMbg21qNkRGBi_1YwEt6D8V2pUWv5U1a4p4-Ma0f4uQG4g0odM-WGomlh7pZWZf3sffiPXk9wBrGisxtCJuaB5vtkheWxpEfWqnhc3QdOWfrsRg6P1h7M95SNVW0U8A38wwrqPOpzEnckCVdCrZz2b2KVln6a4twfINg1-3lEZR4rkEmTaTYlLFlXzbRFWBBPGATxeRxhQ_9N5VhHi7STWPFD5HIJyVqz436bbVvM6Py_oldT_xt_tWlPc0w4Pesy2CgaCPlJCnx6cjEg_sRBUcRkBoHqa7aZj4JFFm9bzEaiJ2MKfkHVT4xdbEimMHsD0HkIQpg5-zoB2Jsqgc6Qi3L57hZi-Q1V0G2lmdZ5WZkQ2m5hxle4hHtAmghgynK2p0qzDWHScxHcdd2sInYqQgMYbnvs04YYKWpIfndCUBs9q_EONN5tn8gfSwHEKlQ-KpplEL5kc-99h2uaZsxRlJOF6_z8EZ-aKaKY8jvoAV0g4kZJH5UKy_MkBiva6r-zXUmo88qJjXQatOOSdfvJUTiZiSfcpBQqF9SSDD9WWsgInaOCKO_fAFf9fuacXDMEj0esUx1YrVEe_77S5uObg-UrK405U1JhKJJvd7o8xQKxenv5BJdsbbyYQDbSSe9BrCqeHEgmfRHTXdSvl_3QOP0Ej-dT8YJJHZ1lrujU7Zg5f5Kg99tU5GdLMbHX2kt4F2a0NX09HikEemvUg4NLPhjOihfVkChr-zdF69nfsnTiaQrMgpIcl9jttN99_8Gju-LU8OWbb92m9RLxAUFP115v22f77YPoILm92IjMZMkxEhGneoclWhnudkyR7YoTBjCnT5b7AC9_05uls637FmVf7Ck8-MF4gLil3dstXi4g24bitYhxxqWwiqF4vsDouSGUnuKCMwx3TLsII_xk77TjpQP4vpLdYM3tn94AVlTMMhnI-OZkVJk-_mIbywCwRHlQb5nzVCc0BWlM1kb9PJys2IfciS8LWEoxeq9moDX5w72yJKoLN3CWpD3VdJAiW79zUaySw-IeW0XaHnlze5fYnOozG8lIeyQ9sMZasMiFovGnR3b7jyMtA38U33v16fouWuBILOu0m_QOpRDI9i3rjRM6hdC48zCtNSzc1_1VPYkWDSFK1oVAjdd8-2rjyqdPeUwnqD26VA9_d3R7x8ThrazdbRC8U1hr9jpqNHuZ4LGYu3Ui8wB-lSt9QMaHz517MY_zBEoNGyvbQWtlM7mvLu12KoMM7nvGrPJnvD-HmxTqsVQolD8_lIV5ao72yiKDpArVr6RuV4PpI0j_Wy4-yDCuwBW0gjnB9GvCwOTeByYXJT6Ul7dgHck4BbF3IyFgvmY--ceWr5mBrbAC9LJP_4Wf5O6ul3hFrhiG6zSV4zzBYLnEwfW6LNLEZjZKgmBYiC5s1xlxYWDdcQ5FGmLQ9uEDkr4VItXQWvIIdeBQPyujxmd965Mig9-Sa7SCyV_3wH8fQnGlvU-jJMGL0zvzB2gcu7hMLMagUBj1AKXj-UxpbX1i95f2TOiZDwMeCCszgvCjQ21XKg07TBXrrOiFcgcADgdo-HJr7O1T3ozOIulq1PDM7QZH6i3wDD0j3b0NCdqKCWqhfLXz2-FszyUHmA_GCzOLVzrLT2DcGWIcQbkvF0yZPgTyqKArKa8qytOerdH6oCJ0bRl96855sMVjuUdyVLX7XW_rVTwsOwV0gVAx8SrzovtDFeHRNl7BQKMsyQ1BjWu25jqKJ598vAi3LCZv0kMdiC24qPdgZU4e2aUkco11EnD6nJgdqsVFxufCl4BD_D9g5Wy42fJt4ZgNPAcbUf341KERyReeBEQj-qlPB3IUTIXcJw68GScebhxb0W_tGKMBC6-ip4QfNW7UTxUxVxmCV7h0yRnBBlkuUR1eYQwWRmEPjKd3dLHvgHtr266NQmE1tnKtJlsdPKb0ztrI9vogsENsgGNFQ2tHoeX8vqxcagGznlPVPfc3DlqjBSeTFQaPWvmCQHVKgxkbvffKzFQyFvXEqt6bGGtkwBoRJ_IIwtSeWQ2nFPBe3rlyKrtSnQFIMibJbbYvPVE03Cld9R61r-GGDSQz4aXekLzePEVwnxpe4mWJGco7ctQyE73PekL1uo2g0bRK-KgaE878OiLRBo5T7c633xEf2hMy9532M2GVdTZuoE0LL-wpAh9GmNdvJZc7g2sINvwZi778v2WHcYEKqXvdmrX-Shyh3QkzgIGZrDzM4UlxxUWaXfZ0Z6PNguk7Jafqf3xuUe9Z8zfAJl5c_VA3k8dn7IRg99hRsh-TGBCzqzgjJq4p4XMWP2QuxFTGSgHRe2GSCFzd5-lPjrj76ZyT3MPUxQe_bV2VE-Oys3MT-VkCCM8jFCANXdrfltG6jSiUZ2uJUWNqdNnxPglmmyrgff_m-5CyIRWYXQsIdZGspqdjzb4F6RbBKfL2PQlM6zUfo9JNmE8YQq815Nxkex8vDOrImnew312fZA6rRjr9_uE4lEbw3U7PFlCKBUPvPnsdgedjKYhiS0xU6iS1NDKOvYhcrkCkiU67EmFD4U0-OCv9Kpbb5bIxTJuv405NxJBElAMVI-ya0ns7D4-xUPn05E7PhtGZT0eHwItjT6omThTsTHwB_bQqYfNrjrObO1l1go2hQ-cUadZYsG5l47CB5RlFhANtaC8tiq4KJi48TmEEApB_0VwOI4EmI7SR0oaqx3HRXZfeGevCx2yC9aCYM4HqcyqP2g_1HwsOYzwq4XDEbK5Yl1dtYABxPoo7t8FBq2sSmfrBJWFv_nvreb_DPwbfoSeCy9knqvOktSQRrPMmo-nNGpandBvjmrjSk3EdeziAP7XNre5I-bn_2voDxkzGFtUM-wzlL379ASRGej8FkNWaOyqGP6Anq5PSJ" +} diff --git a/test/fixtures/keys/ml-kem-1024.json b/test/fixtures/keys/ml-kem-1024.json new file mode 100644 index 00000000000000..f7550808ad1833 --- /dev/null +++ b/test/fixtures/keys/ml-kem-1024.json @@ -0,0 +1,6 @@ +{ + "priv": "XwegslUGcMnzu1c1NccsyRmBaObWSESIGO-ftccj5MvAwjLDAJV_u5KhD_rnJLoLOu0eNGt4p3Oh6Mg5ttxICw", + "kty": "AKP", + "alg": "ML-KEM-1024", + "pub": "DdeBJ4eqvZcmMWheoVWLNhEF81a4GZCrE3Zc2aNgyvY0adJyciCEsFsEvfy3WSXAuRkChxyZtale1ZC0RcVXouFbHEt6mAimzfgaXAojEkVylpOIOppPUZkV8IRiZ4IutPurDNqRXdY3fQmYWwExVSFfoSFDgTgnCUvKvTk79yQoNQucORZ9HFwPj_K7-TpyGbgC7GElMtui4rBHSny0BCJMZeafG8wqK9BcXRN06SmxqRNqsRx9HLJNwphr7IFcMlZTCIxrxoMckfkMe0BiVSAjVNaIB5RvciJbcmR2xqZPEYNTVVKAuIyaJvmLHQEv-ObKfwUnz6WN0NGZjhp7_KYAxYowXjpuThMGGuuSbVyGh8JYzCMx2ng_T0ZxeDG_lAcmHKRTpcdzNThXnsuh9YVzD6aQpyCcaYqxeGVQoex-euQEyPc6PqcsyfHLWxlgPuoCx4p_BAunJCASz_tjClUheiQcCaYAmMmSylOElQwsovdX9uy_NOJ9BmY9cDSMPnaJhFhvIxFR1jNg09ZCuXRYJDiIyWBK2czA3vt9ZVo9o1QnM1SZjvYyCoBO23LHGSZKuHw_ISOeTcMv76qE_4kNlpXLktBxxXmPNBJ7rZC_xaemvkGS8zjGX2Bqf7cGDzKQbEhWk3ovptV1FmN2XykR7DkEa7pOn6iKpCB5yCOr4AHM0ViKqoa7Z3MysSinTianEKVsVLBH0TwKt4c8g0ihCQaITTOtF2QtjYMSIGzHnqVNacJ8yNxDRVLBOqY7G2qXLTp8YUuZ2fCXevBcCQYQNeYQlWURtPMvtiIvpIiIZgc732fFGThZ38bH4xG5KRxD1xq1gLWQZwkYFvhoFSNsVSpfjIJJSaPD76nLk_KfAlQMpQY0SXCzjKGVeoehT7eHrwpQy-oZoThCmYZTI8E5khTCk6h8fis8WAwKnTKLVVuHL2BVDZVG34lp3uR_bUGNDtgNQjJW1LctC6awz0nAtsZUVukbs4VM_Mt-kzZBPtoDPdTDZKGGGiQ3UJAuzisNgdomiloOFlWDWAmSvcYE_qAVnVw66LchMtkGZmEGSCWtIWpxQlNDNvY0nqwGUhzH8ruqlsti30jKu8yE8_J2RdkPKoQre3JcQlZnWaLOvoJ9nhplc7UgG_e229x1XgiTSSy_vLTFPWg_opM6ZhGDwMZwFMORgcExj2HEnlkyVImZeKAtU-GRCgCBKFPLJiNeLSCfNFSGu1NFoBYPxkOBCrUCX5lZt5Um1rYpeemR5otK7Kh_tGJIQUlogeMJTIGS7dZLz0EimmGaaeXAwhN5JPBXkiKPhVhXX4tYb3uaSYsQbtRiw0RdEhysp6EyUrolf-OqOEwbiLJoJiN3hgcP-tuX8ZXIdhNmmyMzD4lIdpZmkkAxD9M64CkK6YFDthCAkHt2E6qgB2liwiwbXNdqU7gH99V2wlycYrKMaKJU5xyzyaQMMUMscapGB8GGRKNhVJwzzka2AxG2tKo-mhllI8ZrcQuHOMQo21jJYkob6xgOhFulAlZ-_icTQGdZ4-g6pzk40ih9rDWfxUYyJNN67-g50jvLIOJrJQEYjcg4TXDBuFahMtTNqOpvAazHFDeqV1iQz0mHBpMMPvGi-Gs5GvBG4kK8m9dMXFtUHvRbToemW2jAzXFjzmW0BEkWs-WMvbwBL3p8mElHZzuNZCsp5EpEzIqDsgktvLHCa5IVN5CrPUzJC9VnMYYrDXIBF4yOLmtFxQmxQdF8RUJc0Oy1aKCA0bchkiuLIiW35tNMd6wDFJctpLoMGxIlNthoWTYYYeFNAEepvZdO_iM4NBd5lACS9pqyuvVx5GmGpFSsPHlyuMWMHQuyH5tf5tCMbgTCkFsvAxo0attaSVkYK6atSBlS8XB0pLQqd_cj4agOy5YSLbACpsI2KfJ1epKFgyB27_yVf7aa4xQt8ksu-ymrgBufVjg92pNzG2dSC0WZR4smS_RmTxZJjvBTVBbNwrilplZROHtWjjkIBNyzNbNu7Ds1c-Jo7WA39WE0awyYc4EVITG1khNS5oULg-AXvXTtwXmUn7GtGxXtlfBpE37RJhm21PYl7WQppG3c0Pk" +} diff --git a/test/fixtures/keys/ml-kem-512.json b/test/fixtures/keys/ml-kem-512.json new file mode 100644 index 00000000000000..0de58f1f5dc47e --- /dev/null +++ b/test/fixtures/keys/ml-kem-512.json @@ -0,0 +1,6 @@ +{ + "priv": "sjVhZaMDzdhH7rMaw7Z9HH-7LfipY2weXpCqr0I3CMzOt_jB1xtlVqz7oLTcOAx1JNA-E7yXLnGPgZdplLmOaA", + "kty": "AKP", + "alg": "ML-KEM-512", + "pub": "2yWZ7mEhbKJopHmZYXjGERQrHBHCQqBThjwVU5gvTPWqPuqAo6cHWrOrHCBt9aGSyPO2BrM0w7O1UZJsadMmhRJaOvN42AvD6OuebvYke_ITOyi2rbE_zww648ox1JF8ZMsTR8ppj_qX_qiAYaFCzpqGG5x_swuYBshuwnSZ2pilOTQ577dcOHNUDUZ5tjdPecuxkpQ_QOhhGJHKzoVY1Yiw4zpPD9kKLxO7BzUbTLcNZSs5VBJ5Ukawy4sfjQa4QNRooMIVCgG7IbVYT-yvgXljdEGxfBYpsewdHXWCF7EHtEmlYsIp3xlPjSbE6FFLQLdqQsiI5qotm4JGPCGvlQsi1AHDyaPC1ZImL4hDG8IXsPxPfwfMS7On5Atn98wxMQo9VSizpps7LFfOMYRH6FmYljbP9BlWesYxGXkZ1DRza0tKrPaZMjAkoRSrGbsHahwdyNtMVnirVyS3OwWmHHSHOyNfwKo2WWVLK2u8pxiVi0d7BCEx7mW4otGhL_iPn8sjieYWGRvJ7Ly0U8areAsFe3KbuHudnAMFw6A38HsIWbbFUjkFb7IJv4WONuUoCqpDknOYjBKUW2U-qtafz-OrLDbPOtBC0iDGTjx90sLHG7WgCbuwfOS1GVcjwNIctqxjXLB-VsEGXOmdzXvJ69MllEB9asBc1AtA86JH8fVMyAYW1xsEbDO6KspBcEtcusJevSKSFGgg0TxrJWWZXtyZ83XBLqVn2AG6WcqE46Zx5tNhrRY7bSIVLPALa6fLPHxmO0CG0lCTlDi-ZwB_GUB7xYBsTcigoIBtNdg_BlqAEVG67jm0zlg3KDMNbyfCBVEpWjAxq4mm5DjMcFaYaXiUv0RnamoSMmG5TXU6HbIC3ZcXs8FX-5N-susyj0wrKkyJI6vCvIOmAnGIvEQj5rc-0qRZZUXIiOdj2rBYk-nAYvqw6pt9h9FVS2g4siaHIZRsjzo5R0h0atWP0ckqTOe13XGM07kvS4wjjYMgawcAfNCUPnR-uMdsy5wuGDx-FWb6gRnhWsPg1S7W-B2grxSPIwotTjc1sKB3eUq0pa4" +} diff --git a/test/fixtures/keys/ml-kem-768.json b/test/fixtures/keys/ml-kem-768.json new file mode 100644 index 00000000000000..858a638b79a661 --- /dev/null +++ b/test/fixtures/keys/ml-kem-768.json @@ -0,0 +1,6 @@ +{ + "priv": "kTfye65c20Ez4T1rDv5YsC_kLexfNxNQHRZohALP3OhhjjiwFoBsHg0-HRLIWO3Nx5OwGhsUToPjfW0OWIU34A", + "kty": "AKP", + "alg": "ML-KEM-768", + "pub": "z9U_gZgaizCOmrqIThIwjRqvJlO8UoFKVrJ3hOQ-Unq9jMKHMJwxsGzDLOAyu-aVbQtJ4slhn8KtXUfF16ln2dxF2cvJDlUHQrrLKLipMKAx5KJh8YMwMhJzUwoRySHGitCikgRDMQozJ-SLmiZd4UY6WfaRL2RHYNkRWxS9I7iIegW3LFc_r-UqP3uew6dcXQXKYOjMg1u9NYg_DLO6f6vB80QnxJI9iMezKmiQHaSokWFuTsYzzHQcbsIg6YoCGvSQQnShzhRJJdhUFxoTZmxZABaGJ1yW0El7N_BWqLBlAVPGbJyGKvOQ4Pos0jGM79Gs_UObJfRykTFK6Ti9fGATnVS98lJu9fkbCYGZTsebqvFED6Ge-QcMu5NTtGiGsDyDY6iqNsS1rqfA-kJ-SaoWDuocAUFqkRHG1nVmEZG4xFZnAjlsO2duiDvD7CrE6CNfVRaapyZh8QbPXmVe5EQH0jEZONc6I7bOeLaznSrCcty1xoF2tCigxOQMbrxlnSanrZgisBpsrxVWpiQD3OXDw4e9njWgiEJhhPcFJpWYS1W0SuUoVJkUT6yR-puk_ENq92Ce6XWFirVwHyWmGCai-acl1_KShFJC_AcuHVQLWACJGxyIVSwQEpMTh1B8sjR6wVafCJKk0eTNuqQwCgd9m7UKK-y61UxpN9RrxffBnKlchpMJkaoimIxUhEEpT1OLh2Zo0QIeYwWn52JxDOh7FlGPZ4N8axsEbzVKAxMuiHWxH-ZJTaWZUzjK-VmW-WmMdJCfCaF63DwGLTYdWiBh3IYqKREx61sJT8F3CsknL_mMLLlHn2Z7L1lztfVtOXckoYR5mwN1ybwnkrxJ_pOqv8GgblEU1GQnBjHGBdaE8sGsqFmDn_rPSmqL2Lpmv_e41LQj7Gxb8XNMtAqDoOM0CBF4KUcPmjm-XjZSDJOFVxoc7NcYWkWiybmW9RSTgVAdzYkep7CAihqMD_R0eiGqehWC1ZTB5_mkJhstbWyjJ7Qs6bdlzqy_hWdeZeQy9sGxnNyF_lik2qfGAKTLwSVhJzBnj2pngBKiYUmq8Mxl2Pg3xjVX73ys51gXX3fES1LCWuXGVtx9dblnYSA2LOCZqqZwZXWPNWCkEvSYQywHM-RwN2dan4F7v1O53bcD2iLGOxXO9-m7bMB7txwpBRdS5aLD9VJjYCmnymcXGOQX9tSdstddX7o5UfB6xrZeXKg-FMyYtnAzM8eJ2vZn1-IYyWoilrzPxftCf2uVU9w2BLZmLCdHvPRPtInFJ8S802KDBvJhJrUAmHgoI9uorjSwOboDpjrIANpoi_t7IpoiS1RAkofDwTxy_wcXt2kBQ1aHSoBhukaIjZZCBwhlE3E-rcaLptUfwGkg3XULcLuxg3F-GSNYWVAffuQS-oUnIrZOmCrCT3xk4tIzy4QX2PlDpuoYjmCsJQQ6o6gfmTYNZzfIZ-hEoxwfFeGyDNZZj-pygbpaOiY2t1dyE0I6PMp3mUKsHMkaXplYoYG7sLy1RvjAISFkmXyC6zmCh5k3QhB7YfygA-qz0Z3cUbIfezcJdafgaqMyR8tSsGyKJO0" +} diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.der b/test/fixtures/keys/ml_dsa_44_private_encrypted.der new file mode 100644 index 00000000000000..2ae136bc7961e5 Binary files /dev/null and b/test/fixtures/keys/ml_dsa_44_private_encrypted.der differ diff --git a/test/fixtures/keys/ml_dsa_44_private_encrypted.pem b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem new file mode 100644 index 00000000000000..e127aa0085bc5f --- /dev/null +++ b/test/fixtures/keys/ml_dsa_44_private_encrypted.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBD1YJCeuwCAuw/ktX9I +K9g9AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQ7kmeI0FzRNLI2m54 +BMASEgRAWBi1BRsuBBVt2kWVTbz8tQa8K3lV+nNE+iRGlMaOhnF5o5Kx4mQnzE1q +ppIFNbWPGGr+xKHTU6fNfNnMecVXKA== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.der b/test/fixtures/keys/ml_kem_768_private_encrypted.der new file mode 100644 index 00000000000000..4f9097751c2249 Binary files /dev/null and b/test/fixtures/keys/ml_kem_768_private_encrypted.der differ diff --git a/test/fixtures/keys/ml_kem_768_private_encrypted.pem b/test/fixtures/keys/ml_kem_768_private_encrypted.pem new file mode 100644 index 00000000000000..0e3d54e75a3259 --- /dev/null +++ b/test/fixtures/keys/ml_kem_768_private_encrypted.pem @@ -0,0 +1,7 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHDMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBSwnAxR1nLC5FZtJyu +lumDAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQyBgMhkKPAMK6jaIc +YxhYcgRg3P97VHfT14YDN024txZznhhzC0mWGNpP6f1EV/mP/YttQp2JTXMKID4V +um3QuQes5my0oOIuiRl3gYIz/BDjKkqLagYBQmUcUUlURgaYJ67Yk3BZg6ULjXmq +EdLYqK5D +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/keys/slh-dsa-sha2-128f.json b/test/fixtures/keys/slh-dsa-sha2-128f.json new file mode 100644 index 00000000000000..73caa631dcb7ef --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-128f.json @@ -0,0 +1,6 @@ +{ + "priv": "kswcPRhjgmqQPbkg5lsMyuQcu-ZOHSWbfRhL-2li-AGkwI9XbgFD6X81JbH9HfJuJXx8vY1hRVUZv5c1qE91yg", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-128f", + "pub": "pMCPV24BQ-l_NSWx_R3ybiV8fL2NYUVVGb-XNahPdco" +} diff --git a/test/fixtures/keys/slh-dsa-sha2-128s.json b/test/fixtures/keys/slh-dsa-sha2-128s.json new file mode 100644 index 00000000000000..05281a8be1bd1b --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-128s.json @@ -0,0 +1,6 @@ +{ + "priv": "VtJ3cgPNS9M7Ykbjo-6joece9TLBphj2bqtaDy4UfvaVMuZCtJp6hEw85ccPOCJ42tAUOYWiwgeXVXoLhEX-LQ", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-128s", + "pub": "lTLmQrSaeoRMPOXHDzgieNrQFDmFosIHl1V6C4RF_i0" +} diff --git a/test/fixtures/keys/slh-dsa-sha2-192f.json b/test/fixtures/keys/slh-dsa-sha2-192f.json new file mode 100644 index 00000000000000..c32fb59e99489a --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-192f.json @@ -0,0 +1,6 @@ +{ + "priv": "mummcFLYg95SHML57yJ4C3w7Mm6xpFXk0Bfk2n4HnieqqaWkMl0idgROda8QK4sNyAWdOpq8UjRAT8Qj8s1L5jTlU5zTqsU1MKGCQ8SqUs9vGFVQZTAflWYmLhsEBAvo", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-192f", + "pub": "yAWdOpq8UjRAT8Qj8s1L5jTlU5zTqsU1MKGCQ8SqUs9vGFVQZTAflWYmLhsEBAvo" +} diff --git a/test/fixtures/keys/slh-dsa-sha2-192s.json b/test/fixtures/keys/slh-dsa-sha2-192s.json new file mode 100644 index 00000000000000..01fad7c03ec028 --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-192s.json @@ -0,0 +1,6 @@ +{ + "priv": "ERjvGyNtYoNScJgtsYHj6GMTEFCATN0iX07PWWSeNSTUheWbFjODji3KxigcdUDnn1nyqNTmhhKgvYKs0TyIPqYh-37_SIf0IHAYIle23Vdm498Z4LL1AVaTPCAO7VEc", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-192s", + "pub": "n1nyqNTmhhKgvYKs0TyIPqYh-37_SIf0IHAYIle23Vdm498Z4LL1AVaTPCAO7VEc" +} diff --git a/test/fixtures/keys/slh-dsa-sha2-256f.json b/test/fixtures/keys/slh-dsa-sha2-256f.json new file mode 100644 index 00000000000000..a7b14928eeef4f --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-256f.json @@ -0,0 +1,6 @@ +{ + "priv": "EIV0GlJ4vcRh5fyncXQaJfPVkNJ35wRlPy5d6UymrEEm9ZzFfK9uVcFDVckvWsuhktzzFbFV_myAZn4Vig6UOMFGET9bxBUOYl2QWI1giMXFNbCADWAm0FFU-gcKeok9y0lM-EjBY75MjDWwKydwvIoHcFRCmnXYqSLZRoT5Mno", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-256f", + "pub": "wUYRP1vEFQ5iXZBYjWCIxcU1sIANYCbQUVT6Bwp6iT3LSUz4SMFjvkyMNbArJ3C8igdwVEKaddipItlGhPkyeg" +} diff --git a/test/fixtures/keys/slh-dsa-sha2-256s.json b/test/fixtures/keys/slh-dsa-sha2-256s.json new file mode 100644 index 00000000000000..333e7cb2fde1bc --- /dev/null +++ b/test/fixtures/keys/slh-dsa-sha2-256s.json @@ -0,0 +1,6 @@ +{ + "priv": "VJxKoGT7hOJhHGCTsaLB3X4wd0MfrXVsaxdLI9Z8MnZZs6DoMTLXduHu0nteVaZmWxgRyWa_mLls3C3T2tfWIhq-QlTkd6f8P4bJ9pWMV4ikXKRnj-ZigYcX7YsGePoroiIZigkam4Oec8vkMBeGUrb_uq0MYzhzsLT2DzoMOwM", + "kty": "AKP", + "alg": "SLH-DSA-SHA2-256s", + "pub": "Gr5CVOR3p_w_hsn2lYxXiKRcpGeP5mKBhxftiwZ4-iuiIhmKCRqbg55zy-QwF4ZStv-6rQxjOHOwtPYPOgw7Aw" +} diff --git a/test/fixtures/keys/slh-dsa-shake-128f.json b/test/fixtures/keys/slh-dsa-shake-128f.json new file mode 100644 index 00000000000000..857e4d433d23e3 --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-128f.json @@ -0,0 +1,6 @@ +{ + "priv": "Z27RiyJvdyCTB2Y5yUY00MPt1YwBseifWehhFgq_qbTUC780VD0Srqs8LsBcwxU6Kb6fH0G0P6H0xO-Hm0GjVw", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-128f", + "pub": "1Au_NFQ9Eq6rPC7AXMMVOim-nx9BtD-h9MTvh5tBo1c" +} diff --git a/test/fixtures/keys/slh-dsa-shake-128s.json b/test/fixtures/keys/slh-dsa-shake-128s.json new file mode 100644 index 00000000000000..97ffc319c4d69e --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-128s.json @@ -0,0 +1,6 @@ +{ + "priv": "g65qYjsKQRA-ms1jkEUhDm6jqivybCJqQce42sa6XHGvpnS_gRLNAKGjZ1fOIh3zW43jtUoRaOnMmYS5BQHbTQ", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-128s", + "pub": "r6Z0v4ESzQCho2dXziId81uN47VKEWjpzJmEuQUB200" +} diff --git a/test/fixtures/keys/slh-dsa-shake-192f.json b/test/fixtures/keys/slh-dsa-shake-192f.json new file mode 100644 index 00000000000000..1007cb4715a126 --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-192f.json @@ -0,0 +1,6 @@ +{ + "priv": "bsd6sYWxjZneB_sh6DQPRtHoaXy4Tk9CM8m_rG1-j9SFEMfhi5R2mI1D0k5aZNH357yJCvE_OYb_ZgESlN_YybjYWF8h8IYv86cfMwDUURrMUJ59UpYvgrIdCVUoiW0g", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-192f", + "pub": "57yJCvE_OYb_ZgESlN_YybjYWF8h8IYv86cfMwDUURrMUJ59UpYvgrIdCVUoiW0g" +} diff --git a/test/fixtures/keys/slh-dsa-shake-192s.json b/test/fixtures/keys/slh-dsa-shake-192s.json new file mode 100644 index 00000000000000..8080b872dffcac --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-192s.json @@ -0,0 +1,6 @@ +{ + "priv": "L3tARiPkHgPWqs0-4NdjUqbiIiyFMSsdIeWLmA9JmiNve4tBXv7O2_k9dHy3l6QZ81CbXK7aoO5xG2E1QimrNax0lwPwM1jMFizaa4YPd6DWCIhzPopoGmBgnS_ZJucR", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-192s", + "pub": "81CbXK7aoO5xG2E1QimrNax0lwPwM1jMFizaa4YPd6DWCIhzPopoGmBgnS_ZJucR" +} diff --git a/test/fixtures/keys/slh-dsa-shake-256f.json b/test/fixtures/keys/slh-dsa-shake-256f.json new file mode 100644 index 00000000000000..9923b9cc4ff9ec --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-256f.json @@ -0,0 +1,6 @@ +{ + "priv": "q7D_kC1JX8USxQPeXS7IlD_E8o0-hR0y2xHANMYvYiX_wKo1AdVNow6qA0mfH-B5teztOAwSh64y3OEfD5r-yLwga0WQtHoIWtDBZ-n5NTkxYf7y3D7PeMvF_Fj0YWB0kCA6-OhCgR9Yo_7yIK9g_0Civ7qRa9h6l_EUZcv68tw", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-256f", + "pub": "vCBrRZC0egha0MFn6fk1OTFh_vLcPs94y8X8WPRhYHSQIDr46EKBH1ij_vIgr2D_QKK_upFr2HqX8RRly_ry3A" +} diff --git a/test/fixtures/keys/slh-dsa-shake-256s.json b/test/fixtures/keys/slh-dsa-shake-256s.json new file mode 100644 index 00000000000000..19f5b448495398 --- /dev/null +++ b/test/fixtures/keys/slh-dsa-shake-256s.json @@ -0,0 +1,6 @@ +{ + "priv": "VPJKCkonGISeCNtpeP8TPBh_zMKyTsEsbvyDuQrAamdFDyyZC9cpQAmzgJZsEhjSpEtXeB8Cf6kSzGkexQVfrSOAVALUzBRSUqj_9ASM0y5BcguvswgdMnhRHX6WagimaY5zg4Urw_wA_E_i1nBn4OEQ8eX3haM3-ZeQAdKu758", + "kty": "AKP", + "alg": "SLH-DSA-SHAKE-256s", + "pub": "I4BUAtTMFFJSqP_0BIzTLkFyC6-zCB0yeFEdfpZqCKZpjnODhSvD_AD8T-LWcGfg4RDx5feFozf5l5AB0q7vnw" +} diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 196f4588188b48..51a3ff10ac2188 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -74,7 +74,7 @@ export const vectors = { [false, { name: 'AES-CBC', length: 25 }], [true, { name: 'AES-GCM', length: 128 }], [false, { name: 'AES-GCM', length: 25 }], - [!boringSSL, { name: 'AES-KW', length: 128 }], + [true, { name: 'AES-KW', length: 128 }], [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], @@ -166,7 +166,7 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], @@ -188,18 +188,18 @@ export const vectors = { [true, 'AES-CTR'], [true, 'AES-CBC'], [true, 'AES-GCM'], - [!boringSSL, 'AES-KW'], + [true, 'AES-KW'], [true, 'Ed25519'], [true, 'X25519'], ], 'wrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], - [!boringSSL, 'AES-KW', 'HMAC'], + [true, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'HMAC'], ], 'unwrapKey': [ [false, 'AES-KW'], - [!boringSSL, 'AES-KW', 'AES-CTR'], + [true, 'AES-KW', 'AES-CTR'], ], 'unsupported operation': [ [false, ''], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index eafb95c559a0f7..2d370b8e21d3d5 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -2,14 +2,13 @@ import * as crypto from 'node:crypto' import { hasOpenSSL } from '../../common/crypto.js' -const pqc = hasOpenSSL(3, 5); +const boringSSL = process.features.openssl_is_boringssl; +const pqc = hasOpenSSL(3, 5) || boringSSL; const argon2 = hasOpenSSL(3, 2); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); -const chacha = crypto.getCiphers().includes('chacha20-poly1305'); const ocb = hasOpenSSL(3); const kmac = hasOpenSSL(3); -const boringSSL = process.features.openssl_is_boringssl; const { subtle } = globalThis.crypto; const X25519 = await subtle.generateKey('X25519', false, ['deriveBits', 'deriveKey']); @@ -28,6 +27,30 @@ export const vectors = { [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], [false, { name: 'cSHAKE256', outputLength: 255 }], + [false, 'TurboSHAKE128'], + [true, { name: 'TurboSHAKE128', outputLength: 128 }], + [true, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], + [false, { name: 'TurboSHAKE128', outputLength: 0 }], + [false, { name: 'TurboSHAKE128', outputLength: 127 }], + [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x00 }], + [false, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x80 }], + [false, 'TurboSHAKE256'], + [true, { name: 'TurboSHAKE256', outputLength: 256 }], + [true, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x07 }], + [false, { name: 'TurboSHAKE256', outputLength: 0 }], + [false, { name: 'TurboSHAKE256', outputLength: 255 }], + [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x00 }], + [false, { name: 'TurboSHAKE256', outputLength: 256, domainSeparation: 0x80 }], + [false, 'KT128'], + [true, { name: 'KT128', outputLength: 128 }], + [true, { name: 'KT128', outputLength: 128, customization: Buffer.alloc(0) }], + [false, { name: 'KT128', outputLength: 0 }], + [false, { name: 'KT128', outputLength: 127 }], + [false, 'KT256'], + [true, { name: 'KT256', outputLength: 256 }], + [true, { name: 'KT256', outputLength: 256, customization: Buffer.alloc(0) }], + [false, { name: 'KT256', outputLength: 0 }], + [false, { name: 'KT256', outputLength: 255 }], ], 'sign': [ [pqc, 'ML-DSA-44'], @@ -51,10 +74,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [false, 'Argon2d'], [false, 'Argon2i'], @@ -72,10 +95,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', length: 128 }], [argon2, 'Argon2d'], [argon2, 'Argon2i'], @@ -93,10 +116,10 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], - [chacha, 'ChaCha20-Poly1305'], + [true, 'ChaCha20-Poly1305'], [ocb, 'AES-OCB'], [false, 'Argon2d'], [false, 'Argon2i'], @@ -117,7 +140,7 @@ export const vectors = { [pqc, 'ML-DSA-44'], [pqc, 'ML-DSA-65'], [pqc, 'ML-DSA-87'], - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [false, 'AES-CTR'], @@ -144,24 +167,27 @@ export const vectors = { 'Argon2id'], ], 'deriveBits': [ - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 2, memory: 16, passes: 1 }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, secretValue: Buffer.alloc(0) }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, associatedData: Buffer.alloc(0) }, 32], - [argon2, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, version: 0x13 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1, version: 0x14 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 7, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 2, memory: 15, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, null], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 24], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 1, memory: 8, passes: 1 }, 31], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 0, memory: 8, passes: 1 }, 32], - [false, { name: 'Argon2d', nonce: Buffer.alloc(0), parallelism: 16777215, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 0 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(7), parallelism: 1, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 2, memory: 16, passes: 1 }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, secretValue: Buffer.alloc(0) }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, associatedData: Buffer.alloc(0) }, 32], + [argon2, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, version: 0x13 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1, version: 0x14 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 7, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 2, memory: 15, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, null], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 24], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 1, memory: 8, passes: 1 }, 31], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 0, memory: 8, passes: 1 }, 32], + [false, { name: 'Argon2d', nonce: Buffer.alloc(8), parallelism: 16777215, memory: 8, passes: 1 }, 32], ], 'encrypt': [ - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12) }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(16) }], - [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], + [true, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], @@ -173,37 +199,39 @@ export const vectors = { [false, 'AES-OCB'], ], 'encapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'encapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], 'decapsulateBits': [ - [pqc, 'ML-KEM-512'], + [pqc && !boringSSL, 'ML-KEM-512'], [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], ], 'decapsulateKey': [ - [pqc, 'ML-KEM-512', 'AES-KW'], - [pqc, 'ML-KEM-512', 'AES-GCM'], - [pqc, 'ML-KEM-512', 'AES-CTR'], - [pqc, 'ML-KEM-512', 'AES-CBC'], - [pqc, 'ML-KEM-512', 'ChaCha20-Poly1305'], - [pqc, 'ML-KEM-512', 'HKDF'], - [pqc, 'ML-KEM-512', 'PBKDF2'], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256' }], - [pqc, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, 'ML-KEM-512', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [pqc && !boringSSL, 'ML-KEM-512', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-KW'], + [pqc, 'ML-KEM-768', 'AES-GCM'], + [pqc, 'ML-KEM-768', 'AES-CTR'], + [pqc, 'ML-KEM-768', 'AES-CBC'], + [pqc, 'ML-KEM-768', 'ChaCha20-Poly1305'], + [pqc, 'ML-KEM-768', 'HKDF'], + [pqc, 'ML-KEM-768', 'PBKDF2'], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256' }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], ], }; diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index b7fb0e9bd6a193..6210354e054d36 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -23,10 +23,10 @@ Last update: - html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing - html/webappapis/structured-clone: https://github.com/web-platform-tests/wpt/tree/47d3fb280c/html/webappapis/structured-clone - html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers -- interfaces: https://github.com/web-platform-tests/wpt/tree/e1b27be06b/interfaces +- interfaces: https://github.com/web-platform-tests/wpt/tree/a8392bd021/interfaces - performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline - resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing -- resources: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/resources +- resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources - streams: https://github.com/web-platform-tests/wpt/tree/f8f26a372f/streams - url: https://github.com/web-platform-tests/wpt/tree/258f285de0/url - urlpattern: https://github.com/web-platform-tests/wpt/tree/f07c03cbed/urlpattern @@ -34,7 +34,8 @@ Last update: - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/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/97bbc7247a/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 - webstorage: https://github.com/web-platform-tests/wpt/tree/1d2c5fb36a/webstorage diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.js index f1609403d623fa..de3b4b5920df27 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.js @@ -51,7 +51,6 @@ function define_tests() { var algorithmName = vector.algorithm; var password = vector.password; - // Key for normal operations promises.push( subtle .importKey('raw-secret', password, algorithmName, false, [ @@ -70,20 +69,3 @@ function define_tests() { }); } } - -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - var aView = new Uint8Array(a); - var bView = new Uint8Array(b); - - for (var i = 0; i < aView.length; i++) { - if (aView[i] !== bView[i]) { - return false; - } - } - - return true; -} diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.tentative.https.any.js index 55fb11c995653e..9422c39c958a06 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2.tentative.https.any.js @@ -1,5 +1,6 @@ // META: title=WebCryptoAPI: deriveBits() Using Argon2 // META: timeout=long +// META: script=../util/helpers.js // META: script=/common/subset-tests.js // META: script=argon2_vectors.js // META: script=argon2.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2_vectors.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2_vectors.js index 9b9acda5f42d3a..93d26b5eb96980 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2_vectors.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/argon2_vectors.js @@ -2,7 +2,6 @@ function getTestData() { // Test vectors from RFC 9106 // https://www.rfc-editor.org/rfc/rfc9106 - // Test vectors from RFC 9106 var testVectors = [ // Argon2d test vector { diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits.js index 8ab9db7bf71318..1406e8bf0a1928 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits.js @@ -224,37 +224,4 @@ function define_tests(algorithmName) { .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveBitsKeys: noDeriveBitsKeys, ecdhKeys: ecdhPublicKeys}}); } - // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is - // omitted, the two values must be the same length and have the same contents - // in every byte. If bitCount is included, only that leading number of bits - // have to match. - function equalBuffers(a, b, bitCount) { - var remainder; - - if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - var length = a.byteLength; - if (typeof bitCount !== "undefined") { - length = Math.floor(bitCount / 8); - } - - for (var i=0; i> (8 - remainder) === bBytes[length] >> (8 - remainder); - } - - return true; - } - } diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js index 866192e0193bc1..5684d7624076c7 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves +// META: script=../util/helpers.js // META: script=cfrg_curves_bits_fixtures.js // META: script=cfrg_curves_bits.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js index 32485c68107e5c..5e482ef0b9d804 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves +// META: script=../util/helpers.js // META: script=cfrg_curves_bits_fixtures.js // META: script=cfrg_curves_bits.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys.js index 62f9e00aa33846..cefc45ac692903 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys.js @@ -221,37 +221,4 @@ function define_tests(algorithmName) { .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, noDeriveKeyKeys: noDeriveKeyKeys, ecdhKeys: ecdhPublicKeys}}); } - // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is - // omitted, the two values must be the same length and have the same contents - // in every byte. If bitCount is included, only that leading number of bits - // have to match. - function equalBuffers(a, b, bitCount) { - var remainder; - - if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - var length = a.byteLength; - if (typeof bitCount !== "undefined") { - length = Math.floor(bitCount / 8); - } - - for (var i=0; i> (8 - remainder) === bBytes[length] >> (8 - remainder); - } - - return true; - } - } diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js index 91390ba5c2a17a..8bcc201d4e95ec 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves +// META: script=../util/helpers.js // META: script=cfrg_curves_bits_fixtures.js // META: script=cfrg_curves_keys.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js index b34e366376a70f..0ed3954ac200b5 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveKey() Using ECDH with CFRG Elliptic Curves +// META: script=../util/helpers.js // META: script=cfrg_curves_bits_fixtures.js // META: script=cfrg_curves_keys.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/derived_bits_length.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/derived_bits_length.https.any.js index 0aee2e3c172d30..862945c6a316c6 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/derived_bits_length.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/derived_bits_length.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveBits() tests for the 'length' parameter +// META: script=../util/helpers.js // META: script=derived_bits_length.js // META: script=derived_bits_length_vectors.js // META: script=derived_bits_length_testcases.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js index 37e3eb4324200c..58a0cecd5efed6 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveBits() Using ECDH +// META: script=../util/helpers.js // META: script=ecdh_bits.js // Define subtests from a `promise_test` to ensure the harness does not diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.js index 36b29c20a282ab..8e79909020d398 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_bits.js @@ -230,37 +230,4 @@ function define_tests() { .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, ecdsaKeyPairs: ecdsaKeyPairs, noDeriveBitsKeys: noDeriveBitsKeys}}); } - // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is - // omitted, the two values must be the same length and have the same contents - // in every byte. If bitCount is included, only that leading number of bits - // have to match. - function equalBuffers(a, b, bitCount) { - var remainder; - - if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - var length = a.byteLength; - if (typeof bitCount !== "undefined") { - length = Math.floor(bitCount / 8); - } - - for (var i=0; i> (8 - remainder) === bBytes[length] >> (8 - remainder); - } - - return true; - } - } diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js index d8235fce5a7412..6464dacfe3aa81 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: deriveKey() Using ECDH +// META: script=../util/helpers.js // META: script=ecdh_keys.js // Define subtests from a `promise_test` to ensure the harness does not diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.js index fce76f185530ac..8c3d2aeb5a4976 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/ecdh_keys.js @@ -209,37 +209,4 @@ function define_tests() { .then(function(results) {return {privateKeys: privateKeys, publicKeys: publicKeys, ecdsaKeyPairs: ecdsaKeyPairs, noDeriveKeyKeys: noDeriveKeyKeys}}); } - // Compares two ArrayBuffer or ArrayBufferView objects. If bitCount is - // omitted, the two values must be the same length and have the same contents - // in every byte. If bitCount is included, only that leading number of bits - // have to match. - function equalBuffers(a, b, bitCount) { - var remainder; - - if (typeof bitCount === "undefined" && a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - var length = a.byteLength; - if (typeof bitCount !== "undefined") { - length = Math.floor(bitCount / 8); - } - - for (var i=0; i> (8 - remainder) === bBytes[length] >> (8 - remainder); - } - - return true; - } - } diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js index 02492c3741c7d1..3879ddb14b903a 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.https.any.js @@ -3,6 +3,7 @@ // META: variant=?1001-2000 // META: variant=?2001-3000 // META: variant=?3001-last +// META: script=../util/helpers.js // META: script=/common/subset-tests.js // META: script=hkdf_vectors.js // META: script=hkdf.js diff --git a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.js b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.js index 0384f88ec73e43..08e8c0c8974617 100644 --- a/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.js +++ b/test/fixtures/wpt/WebCryptoAPI/derive_bits_keys/hkdf.js @@ -275,21 +275,4 @@ function define_tests() { }); } - function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - for (var i=0; i 0) { + promise_test(function (test) { + var buffer = new Uint8Array(input); + // Alter the buffer before calling digest + buffer[0] = ~buffer[0]; + return subtle + .digest({ + get name() { + // Alter the buffer back while calling digest + buffer[0] = input[0]; + return alg; + }, + outputLength: outputLength, + customization: customization, + }, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + }, label + ' and altered buffer during call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + var promise = subtle + .digest(algorithmParams, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + // Alter the buffer after calling digest + buffer[0] = ~buffer[0]; + return promise; + }, label + ' and altered buffer after call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + return subtle + .digest({ + get name() { + // Transfer the buffer while calling digest + buffer.buffer.transfer(); + return alg; + }, + outputLength: outputLength, + customization: customization, + }, buffer) + .then(function (result) { + if (customizationEqual(emptyDataVector, customization) && outputLengthLessOrEqual(emptyDataVector, outputLength)) { + assert_true( + equalBuffers(result, Uint8Array.fromHex(emptyDataVector[2]).subarray(0, outputLength / 8)), + 'digest on transferred buffer should match result for empty buffer' + ); + } else { + assert_equals(result.byteLength, outputLength / 8, + 'digest on transferred buffer should have correct output length'); + } + }); + }, label + ' and transferred buffer during call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + var promise = subtle + .digest(algorithmParams, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + // Transfer the buffer after calling digest + buffer.buffer.transfer(); + return promise; + }, label + ' and transferred buffer after call'); + } + }); +}); diff --git a/test/fixtures/wpt/WebCryptoAPI/digest/sha3.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/digest/sha3.tentative.https.any.js index 4ae99791b8c95f..f9f38eadc2c39a 100644 --- a/test/fixtures/wpt/WebCryptoAPI/digest/sha3.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/digest/sha3.tentative.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: digest() SHA-3 algorithms +// META: script=../util/helpers.js // META: timeout=long var subtle = crypto.subtle; // Change to test prefixed implementations @@ -176,17 +177,3 @@ Object.keys(sourceData).forEach(function (size) { } }); }); - -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - return true; -} diff --git a/test/fixtures/wpt/WebCryptoAPI/digest/turboshake.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/digest/turboshake.tentative.https.any.js new file mode 100644 index 00000000000000..243931cd119802 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/digest/turboshake.tentative.https.any.js @@ -0,0 +1,297 @@ +// META: title=WebCryptoAPI: digest() TurboSHAKE algorithms +// META: script=../util/helpers.js +// META: timeout=long + +var subtle = crypto.subtle; // Change to test prefixed implementations + +// Generates a Uint8Array of length n by repeating the pattern 00 01 02 .. F9 FA. +function ptn(n) { + var buf = new Uint8Array(n); + for (var i = 0; i < n; i++) + buf[i] = i % 251; + return buf; +} + +function hexToBytes(hex) { + var bytes = new Uint8Array(hex.length / 2); + for (var i = 0; i < hex.length; i += 2) + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + return bytes; +} + +// RFC 9861 Section 5 test vectors +// [input, outputLengthBits, expected hex(, domainSeparation)] +var turboSHAKE128Vectors = [ + [new Uint8Array(0), 256, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c'], + [new Uint8Array(0), 512, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c' + + '3e8ccae2a4dae56c84a04c2385c03c15' + + 'e8193bdf58737363321691c05462c8df'], + [ptn(1), 256, + '55cedd6f60af7bb29a4042ae832ef3f5' + + '8db7299f893ebb9247247d856958daa9'], + [ptn(17), 256, + '9c97d036a3bac819db70ede0ca554ec6' + + 'e4c2a1a4ffbfd9ec269ca6a111161233'], + [ptn(Math.pow(17, 2)), 256, + '96c77c279e0126f7fc07c9b07f5cdae1' + + 'e0be60bdbe10620040e75d7223a624d2'], + [ptn(Math.pow(17, 3)), 256, + 'd4976eb56bcf118520582b709f73e1d6' + + '853e001fdaf80e1b13e0d0599d5fb372'], + [ptn(Math.pow(17, 4)), 256, + 'da67c7039e98bf530cf7a37830c6664e' + + '14cbab7f540f58403b1b82951318ee5c'], + [ptn(Math.pow(17, 5)), 256, + 'b97a906fbf83ef7c812517abf3b2d0ae' + + 'a0c4f60318ce11cf103925127f59eecd'], + [ptn(Math.pow(17, 6)), 256, + '35cd494adeded2f25239af09a7b8ef0c' + + '4d1ca4fe2d1ac370fa63216fe7b4c2b1'], + [new Uint8Array([0xff, 0xff, 0xff]), 256, + 'bf323f940494e88ee1c540fe660be8a0' + + 'c93f43d15ec006998462fa994eed5dab', 0x01], + [new Uint8Array([0xff]), 256, + '8ec9c66465ed0d4a6c35d13506718d68' + + '7a25cb05c74cca1e42501abd83874a67', 0x06], + [new Uint8Array([0xff, 0xff, 0xff]), 256, + 'b658576001cad9b1e5f399a9f77723bb' + + 'a05458042d68206f7252682dba3663ed', 0x07], + [new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 256, + '8deeaa1aec47ccee569f659c21dfa8e1' + + '12db3cee37b18178b2acd805b799cc37', 0x0b], + [new Uint8Array([0xff]), 256, + '553122e2135e363c3292bed2c6421fa2' + + '32bab03daa07c7d6636603286506325b', 0x30], + [new Uint8Array([0xff, 0xff, 0xff]), 256, + '16274cc656d44cefd422395d0f9053bd' + + 'a6d28e122aba15c765e5ad0e6eaf26f9', 0x7f], +]; + +var turboSHAKE256Vectors = [ + [new Uint8Array(0), 512, + '367a329dafea871c7802ec67f905ae13' + + 'c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f00' + + '2fbafabb6e13ec1cc20d995547600db0'], + [ptn(1), 512, + '3e1712f928f8eaf1054632b2aa0a246e' + + 'd8b0c378728f60bc970410155c28820e' + + '90cc90d8a3006aa2372c5c5ea176b068' + + '2bf22bae7467ac94f74d43d39b0482e2'], + [ptn(17), 512, + 'b3bab0300e6a191fbe61379398359235' + + '78794ea54843f5011090fa2f3780a9e5' + + 'cb22c59d78b40a0fbff9e672c0fbe097' + + '0bd2c845091c6044d687054da5d8e9c7'], + [ptn(Math.pow(17, 2)), 512, + '66b810db8e90780424c0847372fdc957' + + '10882fde31c6df75beb9d4cd9305cfca' + + 'e35e7b83e8b7e6eb4b78605880116316' + + 'fe2c078a09b94ad7b8213c0a738b65c0'], + [ptn(Math.pow(17, 3)), 512, + 'c74ebc919a5b3b0dd1228185ba02d29e' + + 'f442d69d3d4276a93efe0bf9a16a7dc0' + + 'cd4eabadab8cd7a5edd96695f5d360ab' + + 'e09e2c6511a3ec397da3b76b9e1674fb'], + [ptn(Math.pow(17, 4)), 512, + '02cc3a8897e6f4f6ccb6fd46631b1f52' + + '07b66c6de9c7b55b2d1a23134a170afd' + + 'ac234eaba9a77cff88c1f020b7372461' + + '8c5687b362c430b248cd38647f848a1d'], + [ptn(Math.pow(17, 5)), 512, + 'add53b06543e584b5823f626996aee50' + + 'fe45ed15f20243a7165485acb4aa76b4' + + 'ffda75cedf6d8cdc95c332bd56f4b986' + + 'b58bb17d1778bfc1b1a97545cdf4ec9f'], + [ptn(Math.pow(17, 6)), 512, + '9e11bc59c24e73993c1484ec66358ef7' + + '1db74aefd84e123f7800ba9c4853e02c' + + 'fe701d9e6bb765a304f0dc34a4ee3ba8' + + '2c410f0da70e86bfbd90ea877c2d6104'], + [new Uint8Array([0xff, 0xff, 0xff]), 512, + 'd21c6fbbf587fa2282f29aea620175fb' + + '0257413af78a0b1b2a87419ce031d933' + + 'ae7a4d383327a8a17641a34f8a1d1003' + + 'ad7da6b72dba84bb62fef28f62f12424', 0x01], + [new Uint8Array([0xff]), 512, + '738d7b4e37d18b7f22ad1b5313e357e3' + + 'dd7d07056a26a303c433fa3533455280' + + 'f4f5a7d4f700efb437fe6d281405e07b' + + 'e32a0a972e22e63adc1b090daefe004b', 0x06], + [new Uint8Array([0xff, 0xff, 0xff]), 512, + '18b3b5b7061c2e67c1753a00e6ad7ed7' + + 'ba1c906cf93efb7092eaf27fbeebb755' + + 'ae6e292493c110e48d260028492b8e09' + + 'b5500612b8f2578985ded5357d00ec67', 0x07], + [new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), 512, + 'bb36764951ec97e9d85f7ee9a67a7718' + + 'fc005cf42556be79ce12c0bde50e5736' + + 'd6632b0d0dfb202d1bbb8ffe3dd74cb0' + + '0834fa756cb03471bab13a1e2c16b3c0', 0x0b], + [new Uint8Array([0xff]), 512, + 'f3fe12873d34bcbb2e608779d6b70e7f' + + '86bec7e90bf113cbd4fdd0c4e2f4625e' + + '148dd7ee1a52776cf77f240514d9ccfc' + + '3b5ddab8ee255e39ee389072962c111a', 0x30], + [new Uint8Array([0xff, 0xff, 0xff]), 512, + 'abe569c1f77ec340f02705e7d37c9ab7' + + 'e155516e4a6a150021d70b6fac0bb40c' + + '069f9a9828a0d575cd99f9bae435ab1a' + + 'cf7ed9110ba97ce0388d074bac768776', 0x7f], +]; + +// Large output tests: verify last 32 bytes of extended output +var largeOutputTests = [ + // [algorithm, outputLengthBits, lastNBytes, expectedLastBytes] + ['TurboSHAKE128', 10032 * 8, 32, + 'a3b9b0385900ce761f22aed548e754da' + + '10a5242d62e8c658e3f3a923a7555607'], + ['TurboSHAKE256', 10032 * 8, 32, + 'abefa11630c661269249742685ec082f' + + '207265dccf2f43534e9c61ba0c9d1d75'], +]; + +largeOutputTests.forEach(function (entry) { + var alg = entry[0]; + var outputLength = entry[1]; + var lastN = entry[2]; + var expected = entry[3]; + + promise_test(function (test) { + return subtle + .digest({ name: alg, outputLength: outputLength }, new Uint8Array(0)) + .then(function (result) { + var full = new Uint8Array(result); + var last = full.slice(full.length - lastN); + assert_true( + equalBuffers(last.buffer, hexToBytes(expected)), + 'last ' + lastN + ' bytes of digest match expected' + ); + }); + }, alg + ' with ' + outputLength + ' bit output, verify last ' + lastN + ' bytes'); +}); + +function domainSeparationEqual(emptyDataVector, domainSeparation) { + return (domainSeparation ?? 0x1f) === (emptyDataVector[3] ?? 0x1f); +} + +function outputLengthLessOrEqual(emptyDataVector, outputLength) { + return outputLength <= emptyDataVector[1]; +} + +var allVectors = { + TurboSHAKE128: turboSHAKE128Vectors, + TurboSHAKE256: turboSHAKE256Vectors, +}; + +Object.keys(allVectors).forEach(function (alg) { + var emptyDataVector = allVectors[alg][0]; + allVectors[alg].forEach(function (vector, i) { + var input = vector[0]; + var outputLength = vector[1]; + var expected = vector[2]; + var domainSeparation = vector[3]; + + var algorithmParams = { name: alg, outputLength: outputLength }; + if (domainSeparation !== undefined) + algorithmParams.domainSeparation = domainSeparation; + + var label = alg + ' vector #' + (i + 1) + + ' (' + outputLength + ' bit output, ' + input.length + ' byte input' + + (domainSeparation !== undefined ? ', D=0x' + domainSeparation.toString(16) : '') + ')'; + + promise_test(function (test) { + return subtle + .digest(algorithmParams, input) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + }, label); + + if (input.length > 0) { + promise_test(function (test) { + var buffer = new Uint8Array(input); + // Alter the buffer before calling digest + buffer[0] = ~buffer[0]; + return subtle + .digest({ + get name() { + // Alter the buffer back while calling digest + buffer[0] = input[0]; + return alg; + }, + outputLength: outputLength, + domainSeparation: domainSeparation, + }, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + }, label + ' and altered buffer during call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + var promise = subtle + .digest(algorithmParams, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + // Alter the buffer after calling digest + buffer[0] = ~buffer[0]; + return promise; + }, label + ' and altered buffer after call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + return subtle + .digest({ + get name() { + // Transfer the buffer while calling digest + buffer.buffer.transfer(); + return alg; + }, + outputLength: outputLength, + domainSeparation: domainSeparation, + }, buffer) + .then(function (result) { + if (domainSeparationEqual(emptyDataVector, domainSeparation) && outputLengthLessOrEqual(emptyDataVector, outputLength)) { + assert_true( + equalBuffers(result, Uint8Array.fromHex(emptyDataVector[2]).subarray(0, outputLength / 8)), + 'digest on transferred buffer should match result for empty buffer' + ); + } else { + assert_equals(result.byteLength, outputLength / 8, + 'digest on transferred buffer should have correct output length'); + } + }); + }, label + ' and transferred buffer during call'); + + promise_test(function (test) { + var buffer = new Uint8Array(input); + var promise = subtle + .digest(algorithmParams, buffer) + .then(function (result) { + assert_true( + equalBuffers(result, hexToBytes(expected)), + 'digest matches expected' + ); + }); + // Transfer the buffer after calling digest + buffer.buffer.transfer(); + return promise; + }, label + ' and transferred buffer after call'); + } + }); +}); diff --git a/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_bits.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_bits.tentative.https.any.js index ab112e4d497676..5a669753cd2701 100644 --- a/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_bits.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_bits.tentative.https.any.js @@ -73,7 +73,7 @@ function define_bits_tests() { ); }, algorithmName + ' encapsulateBits basic functionality'); - // Test decapsulateBits operation + // Test encapsulateBits/decapsulateBits round-trip compatibility promise_test(async function (test) { // Generate a key pair for testing var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ @@ -109,30 +109,6 @@ function define_bits_tests() { equalBuffers(decapsulatedBits, encapsulatedBits.sharedKey), 'Decapsulated shared secret should match original' ); - }, algorithmName + ' decapsulateBits basic functionality'); - - // Test round-trip compatibility - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateBits', - 'decapsulateBits', - ]); - - var encapsulatedBits = await subtle.encapsulateBits( - { name: algorithmName }, - keyPair.publicKey - ); - - var decapsulatedBits = await subtle.decapsulateBits( - { name: algorithmName }, - keyPair.privateKey, - encapsulatedBits.ciphertext - ); - - assert_true( - equalBuffers(encapsulatedBits.sharedKey, decapsulatedBits), - 'Encapsulated and decapsulated shared secrets should match' - ); }, algorithmName + ' encapsulateBits/decapsulateBits round-trip compatibility'); @@ -175,19 +151,4 @@ function define_bits_tests() { }); } -// Helper function to compare two ArrayBuffers -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - return true; -} - define_bits_tests(); diff --git a/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.js index 4ccb0585b84f87..0a45c1fc4e9f6f 100644 --- a/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/encap_decap/encap_decap_keys.tentative.https.any.js @@ -38,313 +38,318 @@ function define_key_tests() { variants.forEach(function (algorithmName) { sharedKeyConfigs.forEach(function (config) { - // Test encapsulateKey operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - // Test encapsulateKey - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - config.algorithm, - true, - config.usages - ); - - assert_true( - encapsulatedKey instanceof Object, - 'encapsulateKey should return an object' - ); - assert_true( - encapsulatedKey.hasOwnProperty('sharedKey'), - 'Result should have sharedKey property' - ); - assert_true( - encapsulatedKey.hasOwnProperty('ciphertext'), - 'Result should have ciphertext property' - ); - assert_true( - encapsulatedKey.sharedKey instanceof CryptoKey, - 'sharedKey should be a CryptoKey' - ); - assert_true( - encapsulatedKey.ciphertext instanceof ArrayBuffer, - 'ciphertext should be ArrayBuffer' - ); - - // Verify the shared key properties - assert_equals( - encapsulatedKey.sharedKey.type, - 'secret', - 'Shared key should be secret type' - ); - assert_equals( - encapsulatedKey.sharedKey.algorithm.name, - config.algorithm.name, - 'Shared key algorithm should match' - ); - assert_true( - encapsulatedKey.sharedKey.extractable, - 'Shared key should be extractable as specified' - ); - assert_array_equals( - encapsulatedKey.sharedKey.usages, - config.usages, - 'Shared key should have correct usages' - ); - - // Verify algorithm-specific properties - if (config.algorithm.length) { + [true, false].forEach(function (extractable) { + // Test encapsulateKey operation + promise_test(async function (test) { + // Generate a key pair for testing + var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ + 'encapsulateKey', + 'decapsulateKey', + ]); + + // Test encapsulateKey + var encapsulatedKey = await subtle.encapsulateKey( + { name: algorithmName }, + keyPair.publicKey, + config.algorithm, + extractable, + config.usages + ); + + assert_true( + encapsulatedKey instanceof Object, + 'encapsulateKey should return an object' + ); + assert_true( + encapsulatedKey.hasOwnProperty('sharedKey'), + 'Result should have sharedKey property' + ); + assert_true( + encapsulatedKey.hasOwnProperty('ciphertext'), + 'Result should have ciphertext property' + ); + assert_true( + encapsulatedKey.sharedKey instanceof CryptoKey, + 'sharedKey should be a CryptoKey' + ); + assert_true( + encapsulatedKey.ciphertext instanceof ArrayBuffer, + 'ciphertext should be ArrayBuffer' + ); + + // Verify the shared key properties assert_equals( - encapsulatedKey.sharedKey.algorithm.length, - config.algorithm.length, - 'Key length should be 256' + encapsulatedKey.sharedKey.type, + 'secret', + 'Shared key should be secret type' ); - } - if (config.algorithm.hash) { assert_equals( - encapsulatedKey.sharedKey.algorithm.hash.name, - config.algorithm.hash, - 'Hash algorithm should match' + encapsulatedKey.sharedKey.algorithm.name, + config.algorithm.name, + 'Shared key algorithm should match' ); - } - - // Verify ciphertext length based on algorithm variant - var expectedCiphertextLength; - switch (algorithmName) { - case 'ML-KEM-512': - expectedCiphertextLength = 768; - break; - case 'ML-KEM-768': - expectedCiphertextLength = 1088; - break; - case 'ML-KEM-1024': - expectedCiphertextLength = 1568; - break; - } - assert_equals( - encapsulatedKey.ciphertext.byteLength, - expectedCiphertextLength, - 'Ciphertext should be ' + - expectedCiphertextLength + - ' bytes for ' + - algorithmName - ); - }, algorithmName + ' encapsulateKey with ' + config.description); - - // Test decapsulateKey operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - // First encapsulate to get ciphertext - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - config.algorithm, - true, - config.usages - ); - - // Then decapsulate using the private key - var decapsulatedKey = await subtle.decapsulateKey( - { name: algorithmName }, - keyPair.privateKey, - encapsulatedKey.ciphertext, - config.algorithm, - true, - config.usages - ); - - assert_true( - decapsulatedKey instanceof CryptoKey, - 'decapsulateKey should return a CryptoKey' - ); - assert_equals( - decapsulatedKey.type, - 'secret', - 'Decapsulated key should be secret type' - ); - assert_equals( - decapsulatedKey.algorithm.name, - config.algorithm.name, - 'Decapsulated key algorithm should match' - ); - assert_true( - decapsulatedKey.extractable, - 'Decapsulated key should be extractable as specified' - ); - assert_array_equals( - decapsulatedKey.usages, - config.usages, - 'Decapsulated key should have correct usages' - ); - - // Extract both keys and verify they are identical - var originalKeyMaterial = await subtle.exportKey( - 'raw', - encapsulatedKey.sharedKey - ); - var decapsulatedKeyMaterial = await subtle.exportKey( - 'raw', - decapsulatedKey - ); - - assert_true( - equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), - 'Decapsulated key material should match original' - ); - - // Verify the key material is 32 bytes (256 bits) - assert_equals( - originalKeyMaterial.byteLength, - 32, - 'Shared key material should be 32 bytes' - ); - }, algorithmName + ' decapsulateKey with ' + config.description); - - // Test round-trip compatibility - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - config.algorithm, - true, - config.usages - ); - - var decapsulatedKey = await subtle.decapsulateKey( - { name: algorithmName }, - keyPair.privateKey, - encapsulatedKey.ciphertext, - config.algorithm, - true, - config.usages - ); - - // Verify keys have the same material - var originalKeyMaterial = await subtle.exportKey( - 'raw', - encapsulatedKey.sharedKey - ); - var decapsulatedKeyMaterial = await subtle.exportKey( - 'raw', - decapsulatedKey - ); - - assert_true( - equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), - 'Encapsulated and decapsulated keys should have the same material' - ); - - // Test that the derived keys can actually be used for their intended purpose - if ( - config.algorithm.name.startsWith('AES') && - config.usages.includes('encrypt') - ) { - await testAESOperation( - encapsulatedKey.sharedKey, - decapsulatedKey, - config.algorithm + assert_equals( + encapsulatedKey.sharedKey.extractable, + extractable, + 'Shared key should have correct extractable property' + ); + assert_array_equals( + encapsulatedKey.sharedKey.usages, + config.usages, + 'Shared key should have correct usages' ); - } else if (config.algorithm.name === 'HMAC') { - await testHMACOperation(encapsulatedKey.sharedKey, decapsulatedKey); - } - }, algorithmName + - ' encapsulateKey/decapsulateKey round-trip with ' + - config.description); - }); - // Test vector-based decapsulation for each shared key config - sharedKeyConfigs.forEach(function (config) { - promise_test(async function (test) { - var vectors = ml_kem_vectors[algorithmName]; - - // Import the private key from the vector's privateSeed - var privateKey = await subtle.importKey( - 'raw-seed', - vectors.privateSeed, - { name: algorithmName }, - false, - ['decapsulateKey'] - ); - - // Decapsulate the sample ciphertext from the vectors to get a shared key - var decapsulatedKey = await subtle.decapsulateKey( - { name: algorithmName }, - privateKey, - vectors.sampleCiphertext, - config.algorithm, - true, - config.usages - ); - - assert_true( - decapsulatedKey instanceof CryptoKey, - 'decapsulateKey should return a CryptoKey' - ); - assert_equals( - decapsulatedKey.type, - 'secret', - 'Decapsulated key should be secret type' - ); - assert_equals( - decapsulatedKey.algorithm.name, - config.algorithm.name, - 'Decapsulated key algorithm should match' - ); - assert_true( - decapsulatedKey.extractable, - 'Decapsulated key should be extractable as specified' - ); - assert_array_equals( - decapsulatedKey.usages, - config.usages, - 'Decapsulated key should have correct usages' - ); - - // Extract the key material and verify it matches the expected shared secret - var keyMaterial = await subtle.exportKey('raw', decapsulatedKey); - assert_equals( - keyMaterial.byteLength, - 32, - 'Shared key material should be 32 bytes' - ); - assert_true( - equalBuffers(keyMaterial, vectors.expectedSharedSecret), - "Decapsulated key material should match vector's expectedSharedSecret" - ); - - // Verify algorithm-specific properties - if (config.algorithm.length) { + // Verify algorithm-specific properties + if (config.algorithm.length) { + assert_equals( + encapsulatedKey.sharedKey.algorithm.length, + config.algorithm.length, + 'Key length should be 256' + ); + } + if (config.algorithm.hash) { + assert_equals( + encapsulatedKey.sharedKey.algorithm.hash.name, + config.algorithm.hash, + 'Hash algorithm should match' + ); + } + + // Verify ciphertext length based on algorithm variant + var expectedCiphertextLength; + switch (algorithmName) { + case 'ML-KEM-512': + expectedCiphertextLength = 768; + break; + case 'ML-KEM-768': + expectedCiphertextLength = 1088; + break; + case 'ML-KEM-1024': + expectedCiphertextLength = 1568; + break; + } + assert_equals( + encapsulatedKey.ciphertext.byteLength, + expectedCiphertextLength, + 'Ciphertext should be ' + + expectedCiphertextLength + + ' bytes for ' + + algorithmName + ); + }, `${algorithmName} encapsulateKey with ${config.description} (extractable=${extractable})`); + + // Test decapsulateKey operation + promise_test(async function (test) { + // Generate a key pair for testing + var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ + 'encapsulateKey', + 'decapsulateKey', + ]); + + // First encapsulate to get ciphertext + var encapsulatedKey = await subtle.encapsulateKey( + { name: algorithmName }, + keyPair.publicKey, + config.algorithm, + extractable, + config.usages + ); + + // Then decapsulate using the private key + var decapsulatedKey = await subtle.decapsulateKey( + { name: algorithmName }, + keyPair.privateKey, + encapsulatedKey.ciphertext, + config.algorithm, + extractable, + config.usages + ); + + assert_true( + decapsulatedKey instanceof CryptoKey, + 'decapsulateKey should return a CryptoKey' + ); + assert_equals( + decapsulatedKey.type, + 'secret', + 'Decapsulated key should be secret type' + ); + assert_equals( + decapsulatedKey.algorithm.name, + config.algorithm.name, + 'Decapsulated key algorithm should match' + ); + assert_equals( + decapsulatedKey.extractable, + extractable, + 'Decapsulated key should have correct extractable property' + ); + assert_array_equals( + decapsulatedKey.usages, + config.usages, + 'Decapsulated key should have correct usages' + ); + + if (extractable) { + // Extract both keys and verify they are identical + var originalKeyMaterial = await subtle.exportKey( + 'raw', + encapsulatedKey.sharedKey + ); + var decapsulatedKeyMaterial = await subtle.exportKey( + 'raw', + decapsulatedKey + ); + + assert_true( + equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), + 'Decapsulated key material should match original' + ); + + // Verify the key material is 32 bytes (256 bits) + assert_equals( + originalKeyMaterial.byteLength, + 32, + 'Shared key material should be 32 bytes' + ); + } + }, `${algorithmName} decapsulateKey with ${config.description} (extractable=${extractable})`); + + // Test round-trip compatibility + promise_test(async function (test) { + var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ + 'encapsulateKey', + 'decapsulateKey', + ]); + + var encapsulatedKey = await subtle.encapsulateKey( + { name: algorithmName }, + keyPair.publicKey, + config.algorithm, + extractable, + config.usages + ); + + var decapsulatedKey = await subtle.decapsulateKey( + { name: algorithmName }, + keyPair.privateKey, + encapsulatedKey.ciphertext, + config.algorithm, + extractable, + config.usages + ); + + if (extractable) { + // Verify keys have the same material + var originalKeyMaterial = await subtle.exportKey( + 'raw', + encapsulatedKey.sharedKey + ); + var decapsulatedKeyMaterial = await subtle.exportKey( + 'raw', + decapsulatedKey + ); + + assert_true( + equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), + 'Encapsulated and decapsulated keys should have the same material' + ); + } + + // Test that the derived keys can actually be used for their intended purpose + if ( + config.algorithm.name.startsWith('AES') && + config.usages.includes('encrypt') + ) { + await testAESOperation( + encapsulatedKey.sharedKey, + decapsulatedKey, + config.algorithm + ); + } else if (config.algorithm.name === 'HMAC') { + await testHMACOperation(encapsulatedKey.sharedKey, decapsulatedKey); + } + }, `${algorithmName} encapsulateKey/decapsulateKey round-trip with ${config.description} (extractable=${extractable})`); + + // Test vector-based decapsulation + promise_test(async function (test) { + var vectors = ml_kem_vectors[algorithmName]; + + // Import the private key from the vector's privateSeed + var privateKey = await subtle.importKey( + 'raw-seed', + vectors.privateSeed, + { name: algorithmName }, + false, + ['decapsulateKey'] + ); + + // Decapsulate the sample ciphertext from the vectors to get a shared key + var decapsulatedKey = await subtle.decapsulateKey( + { name: algorithmName }, + privateKey, + vectors.sampleCiphertext, + config.algorithm, + extractable, + config.usages + ); + + assert_true( + decapsulatedKey instanceof CryptoKey, + 'decapsulateKey should return a CryptoKey' + ); assert_equals( - decapsulatedKey.algorithm.length, - config.algorithm.length, - 'Key length should be 256' + decapsulatedKey.type, + 'secret', + 'Decapsulated key should be secret type' ); - } - if (config.algorithm.hash) { assert_equals( - decapsulatedKey.algorithm.hash.name, - config.algorithm.hash, - 'Hash algorithm should match' + decapsulatedKey.algorithm.name, + config.algorithm.name, + 'Decapsulated key algorithm should match' + ); + assert_equals( + decapsulatedKey.extractable, + extractable, + 'Decapsulated key should have correct extractable property' + ); + assert_array_equals( + decapsulatedKey.usages, + config.usages, + 'Decapsulated key should have correct usages' ); - } - }, algorithmName + - ' vector-based sampleCiphertext decapsulation with ' + - config.description); + + if (extractable) { + // Extract the key material and verify it matches the expected shared secret + var keyMaterial = await subtle.exportKey('raw', decapsulatedKey); + assert_equals( + keyMaterial.byteLength, + 32, + 'Shared key material should be 32 bytes' + ); + assert_true( + equalBuffers(keyMaterial, vectors.expectedSharedSecret), + "Decapsulated key material should match vector's expectedSharedSecret" + ); + } + + // Verify algorithm-specific properties + if (config.algorithm.length) { + assert_equals( + decapsulatedKey.algorithm.length, + config.algorithm.length, + 'Key length should be 256' + ); + } + if (config.algorithm.hash) { + assert_equals( + decapsulatedKey.algorithm.hash.name, + config.algorithm.hash, + 'Hash algorithm should match' + ); + } + }, `${algorithmName} vector-based sampleCiphertext decapsulation with ${config.description} (extractable=${extractable})`); + }); }); }); } @@ -430,19 +435,4 @@ async function testHMACOperation(key1, key2) { assert_true(verified2, 'HMAC verification should succeed with key2'); } -// Helper function to compare two ArrayBuffers -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - return true; -} - define_key_tests(); diff --git a/test/fixtures/wpt/WebCryptoAPI/encap_decap/ml_kem_encap_decap.js b/test/fixtures/wpt/WebCryptoAPI/encap_decap/ml_kem_encap_decap.js deleted file mode 100644 index 9167efa63f0ca3..00000000000000 --- a/test/fixtures/wpt/WebCryptoAPI/encap_decap/ml_kem_encap_decap.js +++ /dev/null @@ -1,410 +0,0 @@ -// Test implementation for ML-KEM encapsulate and decapsulate operations - -function define_tests() { - var subtle = self.crypto.subtle; - - // Test data for all ML-KEM variants - var variants = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; - - variants.forEach(function (algorithmName) { - var testVector = ml_kem_vectors[algorithmName]; - - // Test encapsulateBits operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateBits', - 'decapsulateBits', - ]); - - // Test encapsulateBits - var encapsulatedBits = await subtle.encapsulateBits( - { name: algorithmName }, - keyPair.publicKey - ); - - assert_true( - encapsulatedBits instanceof Object, - 'encapsulateBits should return an object' - ); - assert_true( - encapsulatedBits.hasOwnProperty('sharedKey'), - 'Result should have sharedKey property' - ); - assert_true( - encapsulatedBits.hasOwnProperty('ciphertext'), - 'Result should have ciphertext property' - ); - assert_true( - encapsulatedBits.sharedKey instanceof ArrayBuffer, - 'sharedKey should be ArrayBuffer' - ); - assert_true( - encapsulatedBits.ciphertext instanceof ArrayBuffer, - 'ciphertext should be ArrayBuffer' - ); - - // Verify sharedKey length (should be 32 bytes for all ML-KEM variants) - assert_equals( - encapsulatedBits.sharedKey.byteLength, - 32, - 'Shared key should be 32 bytes' - ); - - // Verify ciphertext length based on algorithm variant - var expectedCiphertextLength; - switch (algorithmName) { - case 'ML-KEM-512': - expectedCiphertextLength = 768; - break; - case 'ML-KEM-768': - expectedCiphertextLength = 1088; - break; - case 'ML-KEM-1024': - expectedCiphertextLength = 1568; - break; - } - assert_equals( - encapsulatedBits.ciphertext.byteLength, - expectedCiphertextLength, - 'Ciphertext should be ' + - expectedCiphertextLength + - ' bytes for ' + - algorithmName - ); - }, algorithmName + ' encapsulateBits basic functionality'); - - // Test decapsulateBits operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateBits', - 'decapsulateBits', - ]); - - // First encapsulate to get ciphertext - var encapsulatedBits = await subtle.encapsulateBits( - { name: algorithmName }, - keyPair.publicKey - ); - - // Then decapsulate using the private key - var decapsulatedBits = await subtle.decapsulateBits( - { name: algorithmName }, - keyPair.privateKey, - encapsulatedBits.ciphertext - ); - - assert_true( - decapsulatedBits instanceof ArrayBuffer, - 'decapsulateBits should return ArrayBuffer' - ); - assert_equals( - decapsulatedBits.byteLength, - 32, - 'Decapsulated bits should be 32 bytes' - ); - - // The decapsulated shared secret should match the original - assert_true( - equalBuffers(decapsulatedBits, encapsulatedBits.sharedKey), - 'Decapsulated shared secret should match original' - ); - }, algorithmName + ' decapsulateBits basic functionality'); - - // Test encapsulateKey operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - // Test encapsulateKey with AES-GCM as the shared key algorithm - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - assert_true( - encapsulatedKey instanceof Object, - 'encapsulateKey should return an object' - ); - assert_true( - encapsulatedKey.hasOwnProperty('sharedKey'), - 'Result should have sharedKey property' - ); - assert_true( - encapsulatedKey.hasOwnProperty('ciphertext'), - 'Result should have ciphertext property' - ); - assert_true( - encapsulatedKey.sharedKey instanceof CryptoKey, - 'sharedKey should be a CryptoKey' - ); - assert_true( - encapsulatedKey.ciphertext instanceof ArrayBuffer, - 'ciphertext should be ArrayBuffer' - ); - - // Verify the shared key properties - assert_equals( - encapsulatedKey.sharedKey.type, - 'secret', - 'Shared key should be secret type' - ); - assert_equals( - encapsulatedKey.sharedKey.algorithm.name, - 'AES-GCM', - 'Shared key algorithm should be AES-GCM' - ); - assert_equals( - encapsulatedKey.sharedKey.algorithm.length, - 256, - 'Shared key length should be 256' - ); - assert_true( - encapsulatedKey.sharedKey.extractable, - 'Shared key should be extractable as specified' - ); - assert_array_equals( - encapsulatedKey.sharedKey.usages, - ['encrypt', 'decrypt'], - 'Shared key should have correct usages' - ); - }, algorithmName + ' encapsulateKey basic functionality'); - - // Test decapsulateKey operation - promise_test(async function (test) { - // Generate a key pair for testing - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - // First encapsulate to get ciphertext - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - // Then decapsulate using the private key - var decapsulatedKey = await subtle.decapsulateKey( - { name: algorithmName }, - keyPair.privateKey, - encapsulatedKey.ciphertext, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - assert_true( - decapsulatedKey instanceof CryptoKey, - 'decapsulateKey should return a CryptoKey' - ); - assert_equals( - decapsulatedKey.type, - 'secret', - 'Decapsulated key should be secret type' - ); - assert_equals( - decapsulatedKey.algorithm.name, - 'AES-GCM', - 'Decapsulated key algorithm should be AES-GCM' - ); - assert_equals( - decapsulatedKey.algorithm.length, - 256, - 'Decapsulated key length should be 256' - ); - assert_true( - decapsulatedKey.extractable, - 'Decapsulated key should be extractable as specified' - ); - assert_array_equals( - decapsulatedKey.usages, - ['encrypt', 'decrypt'], - 'Decapsulated key should have correct usages' - ); - - // Extract both keys and verify they are identical - var originalKeyMaterial = await subtle.exportKey( - 'raw', - encapsulatedKey.sharedKey - ); - var decapsulatedKeyMaterial = await subtle.exportKey( - 'raw', - decapsulatedKey - ); - - assert_true( - equalBuffers(originalKeyMaterial, decapsulatedKeyMaterial), - 'Decapsulated key material should match original' - ); - }, algorithmName + ' decapsulateKey basic functionality'); - - // Test error cases for encapsulateBits - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateBits', - 'decapsulateBits', - ]); - - // Test with wrong key type (private key instead of public) - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.encapsulateBits({ name: algorithmName }, keyPair.privateKey), - 'encapsulateBits should reject private key' - ); - - // Test with wrong algorithm name - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.encapsulateBits({ name: 'AES-GCM' }, keyPair.publicKey), - 'encapsulateBits should reject mismatched algorithm' - ); - }, algorithmName + ' encapsulateBits error cases'); - - // Test error cases for decapsulateBits - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateBits', - 'decapsulateBits', - ]); - - var encapsulatedBits = await subtle.encapsulateBits( - { name: algorithmName }, - keyPair.publicKey - ); - - // Test with wrong key type (public key instead of private) - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.decapsulateBits( - { name: algorithmName }, - keyPair.publicKey, - encapsulatedBits.ciphertext - ), - 'decapsulateBits should reject public key' - ); - - // Test with wrong algorithm name - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.decapsulateBits( - { name: 'AES-GCM' }, - keyPair.privateKey, - encapsulatedBits.ciphertext - ), - 'decapsulateBits should reject mismatched algorithm' - ); - - // Test with invalid ciphertext - var invalidCiphertext = new Uint8Array(10); // Wrong size - await promise_rejects_dom( - test, - 'OperationError', - subtle.decapsulateBits( - { name: algorithmName }, - keyPair.privateKey, - invalidCiphertext - ), - 'decapsulateBits should reject invalid ciphertext' - ); - }, algorithmName + ' decapsulateBits error cases'); - - // Test error cases for encapsulateKey - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - // Test with key without encapsulateKey usage - var wrongKeyPair = await subtle.generateKey( - { name: algorithmName }, - false, - ['decapsulateKey'] // Missing encapsulateKey usage - ); - - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.encapsulateKey( - { name: algorithmName }, - wrongKeyPair.publicKey, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ), - 'encapsulateKey should reject key without encapsulateKey usage' - ); - }, algorithmName + ' encapsulateKey error cases'); - - // Test error cases for decapsulateKey - promise_test(async function (test) { - var keyPair = await subtle.generateKey({ name: algorithmName }, false, [ - 'encapsulateKey', - 'decapsulateKey', - ]); - - var encapsulatedKey = await subtle.encapsulateKey( - { name: algorithmName }, - keyPair.publicKey, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ); - - // Test with key without decapsulateKey usage - var wrongKeyPair = await subtle.generateKey( - { name: algorithmName }, - false, - ['encapsulateKey'] // Missing decapsulateKey usage - ); - - await promise_rejects_dom( - test, - 'InvalidAccessError', - subtle.decapsulateKey( - { name: algorithmName }, - wrongKeyPair.privateKey, - encapsulatedKey.ciphertext, - { name: 'AES-GCM', length: 256 }, - true, - ['encrypt', 'decrypt'] - ), - 'decapsulateKey should reject key without decapsulateKey usage' - ); - }, algorithmName + ' decapsulateKey error cases'); - }); -} - -// Helper function to compare two ArrayBuffers -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - return true; -} - -function run_test() { - define_tests(); -} diff --git a/test/fixtures/wpt/WebCryptoAPI/encrypt_decrypt/aes.js b/test/fixtures/wpt/WebCryptoAPI/encrypt_decrypt/aes.js index 456f66423419c8..879a6efe257e49 100644 --- a/test/fixtures/wpt/WebCryptoAPI/encrypt_decrypt/aes.js +++ b/test/fixtures/wpt/WebCryptoAPI/encrypt_decrypt/aes.js @@ -482,34 +482,5 @@ function run_test() { } } - // Returns a copy of the sourceBuffer it is sent. - function copyBuffer(sourceBuffer) { - var source = new Uint8Array(sourceBuffer); - var copy = new Uint8Array(sourceBuffer.byteLength) - - for (var i=0; i { + idl_array.add_objects({ + Crypto: ['crypto'], + SubtleCrypto: ['crypto.subtle'] + }); + } +); diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-DSA_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-DSA_importKey.js index 3723b321e542d5..d9257ac6982505 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-DSA_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-DSA_importKey.js @@ -106,44 +106,6 @@ function testFormat(format, algorithm, keyData, keySize, usages, extractable) { // Helper methods follow: -// Are two array buffers the same? -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - - return true; -} - -// Are two Jwk objects "the same"? That is, does the object returned include -// matching values for each property that was expected? It's okay if the -// returned object has extra methods; they aren't checked. -function equalJwk(expected, got) { - var fields = Object.keys(expected); - var fieldName; - - for (var i = 0; i < fields.length; i++) { - fieldName = fields[i]; - if (!(fieldName in got)) { - return false; - } - if (expected[fieldName] !== got[fieldName]) { - return false; - } - } - - return true; -} - // Convert method parameters to a string to uniquely name each test function parameterString(format, data, algorithm, extractable, usages) { if ('byteLength' in data) { @@ -166,57 +128,3 @@ function parameterString(format, data, algorithm, extractable, usages) { return result; } - -// Character representation of any object we may use as a parameter. -function objectToString(obj) { - var keyValuePairs = []; - - if (Array.isArray(obj)) { - return ( - '[' + - obj - .map(function (elem) { - return objectToString(elem); - }) - .join(', ') + - ']' - ); - } else if (typeof obj === 'object') { - Object.keys(obj) - .sort() - .forEach(function (keyName) { - keyValuePairs.push(keyName + ': ' + objectToString(obj[keyName])); - }); - return '{' + keyValuePairs.join(', ') + '}'; - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else { - return obj.toString(); - } - - var keyValuePairs = []; - - Object.keys(obj) - .sort() - .forEach(function (keyName) { - var value = obj[keyName]; - if (typeof value === 'object') { - value = objectToString(value); - } else if (typeof value === 'array') { - value = - '[' + - value - .map(function (elem) { - return objectToString(elem); - }) - .join(', ') + - ']'; - } else { - value = value.toString(); - } - - keyValuePairs.push(keyName + ': ' + value); - }); - - return '{' + keyValuePairs.join(', ') + '}'; -} diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js index c264a163148438..d9257ac6982505 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey.js @@ -3,14 +3,14 @@ var subtle = crypto.subtle; function runTests(algorithmName) { var algorithm = { name: algorithmName }; var data = keyData[algorithmName]; - // var jwkData = {jwk: {kty: data.jwk.kty, alg: data.jwk.alg, pub: data.jwk.pub}}; - - // TODO: add JWK when its definition is done in IETF JOSE WG + var jwkData = { + jwk: { kty: data.jwk.kty, alg: data.jwk.alg, pub: data.jwk.pub }, + }; [true, false].forEach(function (extractable) { // Test public keys first allValidUsages(data.publicUsages, true).forEach(function (usages) { - ['spki', /*'jwk',*/ 'raw-public'].forEach(function (format) { + ['spki', 'jwk', 'raw-public'].forEach(function (format) { if (format === 'jwk') { // Not all fields used for public keys testFormat( @@ -36,7 +36,7 @@ function runTests(algorithmName) { // Next, test private keys allValidUsages(data.privateUsages).forEach(function (usages) { - ['pkcs8', /*'jwk',*/ 'raw-seed'].forEach(function (format) { + ['pkcs8', 'jwk', 'raw-seed'].forEach(function (format) { testFormat(format, algorithm, data, algorithmName, usages, extractable); }); }); @@ -106,44 +106,6 @@ function testFormat(format, algorithm, keyData, keySize, usages, extractable) { // Helper methods follow: -// Are two array buffers the same? -function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - for (var i = 0; i < a.byteLength; i++) { - if (aBytes[i] !== bBytes[i]) { - return false; - } - } - - return true; -} - -// Are two Jwk objects "the same"? That is, does the object returned include -// matching values for each property that was expected? It's okay if the -// returned object has extra methods; they aren't checked. -function equalJwk(expected, got) { - var fields = Object.keys(expected); - var fieldName; - - for (var i = 0; i < fields.length; i++) { - fieldName = fields[i]; - if (!(fieldName in got)) { - return false; - } - if (expected[fieldName] !== got[fieldName]) { - return false; - } - } - - return true; -} - // Convert method parameters to a string to uniquely name each test function parameterString(format, data, algorithm, extractable, usages) { if ('byteLength' in data) { @@ -166,57 +128,3 @@ function parameterString(format, data, algorithm, extractable, usages) { return result; } - -// Character representation of any object we may use as a parameter. -function objectToString(obj) { - var keyValuePairs = []; - - if (Array.isArray(obj)) { - return ( - '[' + - obj - .map(function (elem) { - return objectToString(elem); - }) - .join(', ') + - ']' - ); - } else if (typeof obj === 'object') { - Object.keys(obj) - .sort() - .forEach(function (keyName) { - keyValuePairs.push(keyName + ': ' + objectToString(obj[keyName])); - }); - return '{' + keyValuePairs.join(', ') + '}'; - } else if (typeof obj === 'undefined') { - return 'undefined'; - } else { - return obj.toString(); - } - - var keyValuePairs = []; - - Object.keys(obj) - .sort() - .forEach(function (keyName) { - var value = obj[keyName]; - if (typeof value === 'object') { - value = objectToString(value); - } else if (typeof value === 'array') { - value = - '[' + - value - .map(function (elem) { - return objectToString(elem); - }) - .join(', ') + - ']'; - } else { - value = value.toString(); - } - - keyValuePairs.push(keyName + ': ' + value); - }); - - return '{' + keyValuePairs.join(', ') + '}'; -} diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js index 1c25d37614e92e..f3a5f6687dc5e1 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ML-KEM_importKey_fixtures.js @@ -23,6 +23,12 @@ var keyData = { 80, 130, 235, 161, 49, 141, 160, 11, 40, 152, 18, 142, 30, 56, 252, 129, 211, ]), + jwk: { + priv: 'pSbBpIR6aK1T1uMf57eY5GCMQRdTgwjN9cB64vRehm12PK3LGUu9kBYlozVOlblQguuhMY2gCyiYEo4eOPyB0w', + kty: 'AKP', + alg: 'ML-KEM-512', + pub: 'NmgRwWK5Bbwb-FQJRnK_kDCVThmIH7wdYMRUaaUaIFQoZJQK5bJh0zEtyFpBboqtcZLG7tibd5wJQpNDnnkvmHZjC3QZl2iOLolHnjQPe7FuOPGP_EqSF0kziAZ7cHQ2UPgJVcVhQWYsqhdjAkEsTqyrUvOlclGMjxSOdNF0e9qVxWZRjvFzTudlmfKGdPsO9gmnKRYsHqbDm4mdVJOlRhpeZIUUwPRF3KrIUBul5CpUPVuUdPrM1Ola15ISYJU_mDI0ECKpElWtiAmRcOWxoiGd3tGWwPyorGqmJCabuEI7ZaqjIQoygXlWanvIxEVI75d2aroa4ZM1dgMJAiWM_Pcet7EEieafA7pdP1EQ6utJs9oYDXsngBZaOXiqc_hnm3KzKQUZIkiPA9BQgxN53RMG51ZC2HVHwoC7iaVQsDi4u9iFRZCplXWPZXQMcSpH_sBy4cEVDgW-AZweDpIW9rICT9xOHwwwmvWtNMmXmDZ0-vNN14kHGdBOP4N0-YIj1nwdkwIHskKfAnlDVcrA0Rx-EFixxcQiXDq-xeBc-gY6UdR6LDQeifp7BRwqTicqBYOCdYHJpIN2pweK70puM3Iy_kHIPhIazCBnJvOKl3YlYRsPR8BU12W3LJS4kOUfvXiNqIKrqXxFv7BxURULIfvN7myEBrc9OpivU_IfiOOy-5jA8RRxLGsWnppqQdINFuqvYtEn31obk2CVnESA7hnOERuM7EOdU-M8MsN0QSlmZJxAN3VavFBl6uOq8FJJvfydUvEWc3vPrnB2g_KmXuwtprObdlO9_1JWp2UnenFpOClD7QnMKtaiBmWOGZZV6dmIWrtY9_RZ01Gn_LORv8JoWQhAb2tMrRVtF9M1Jjx0jIO5jlSx9MgVzYte18YCMTCPY-J5Zuo3Q6o-LdLInnUq6cVugdQMjhJ_8SuXmsBYIHXN9FMKttWI6YKcLVYjHstsXiEP0kOx4fd4kZaKH-S3vCIl9CA5BiUXjxK108ooEpY9lTxH_yGCQRnEkve0HwWwHfvagpKsDDWzRIh4xAHRsY9_xcPO-JlXbUZ6_HA', + }, }, 'ML-KEM-768': { privateUsages: ['decapsulateBits', 'decapsulateKey'], @@ -47,6 +53,12 @@ var keyData = { 201, 9, 240, 170, 192, 86, 52, 228, 122, 16, 73, 139, 30, 11, 224, 117, 143, 2, 124, 21, 63, 91, 210, 94, 160, 123, 99, 189, 172, 61, 235, 191, ]), + jwk: { + priv: 'psgdZMYwUKSP1wJsaqLna2-PAg63_ZwRW9VLyHBZ8zfJCfCqwFY05HoQSYseC-B1jwJ8FT9b0l6ge2O9rD3rvw', + kty: 'AKP', + alg: 'ML-KEM-768', + pub: '7lhoaxGSnpU0O6dvndAgrKiCDLwlHeMBR4ZatDw5lYwtPCIEI9CbmfqjX7kTAcoJsEKh-oityrERRVegtAmShCZFdzGGVjRh3Yh594KhKZfGPYyUnTq8eBETGfl0Gldu8BBimWuzEGhghoOHYPa0ygEfUQZhU8qZ8nA6TbiuNSdPJ8xdfxtgiEiCe3Z9SqhF3aerJoyuc2uAKbGPafrCd7rDsitzFPI1TVYaKWC1OblyKVu8PhWtEdx8VYRftaUJBwfKEnQSNvWLZTVEbTqgJQNwHLmQyMSktnjNKDmrF2C-TkKFOrenC4QAEGQvcrAuidpcuPNl8FVMDKUsIRY5aiQvEsdRxJcWZGJ6hUVzzseVrgBM0NFrIDkpe5Wga-hSZggbACIvWzISJgNkznp7oJM36FxeVbkHDXTNaLQ-PHR7pTwrpzUCmiecewGZ5ZIE6ooPaMC84vdxlqB-WKQV2gs_t1mO20NnZcCyqPkGNNtueOkGZlJORvhYBbqEhdKdtLF4b-oo-CBrHgAKRGVaAGoFEleSCJFusCsX_ajBKQkvNSdJXDJC82QGXbnKL1sK9QLOzTYV_xgZWMW3tRFYMMWSz5w000irtFDNcmJcUbkn0BGg-vdzceY46eOeg3JE2rh-9fVLzhZMq6UG8YyhBZMonEx-JalyninCurYWk1aEVic0n7cQ1MMPvnJyKolXubWNKFGiB7K1H2mG5TafRrJvCQlXfSoncaLHr9SGr2UQvyvGNMTGYcFIxnCp3lyU42a157x3OwINDcA39OLD0hJ7JhdeVGkeIQp9VRt1vaOUEEPFVXuXIPtBqMu-9gOfL3hNh2aixMCP6VS6gypYfYNNCTZ9O7pJ81sFfEJv4-yR5zBZ0IYi4saeeoUJQgmohBcm9IaUDoWAlVhbxKoV1SQ22juWvXas5flgyzVh-GV7bpVoH-SawqlAEnxE-vWWVXLMRXYuh_CByxYvFPVCYeS4GuKF9kpT7rWQ6gM-T-gV5zS62EQ7qrxZL7DOjkqXnqeaD7pMxWRKt4QFdYupnNgfI1wt8Mxjn_CzKOOBHZIdKooppSI2BOpmvyu1zcDJX1snbBNcLVtMdbBYJXOf1RXIEXWCAOKf1jNGLRyOazhlbOpmtBxRsqO5-Lq9odptmUO3FtQ23VqQ0nhFHeFO8yMpOAp2H3widhpFQbKbYKKrtfjGL_kXwxqlh4k1nAmCXzvKe7wzlyow1NgEx_e6MRkc_PIsekgytkAhsPSYc0t_AuEy5xyqN_m_JMbLJCwaFVtoPVoKPNRzFtM1YrNSrtAOg0MLMna5eJN9CotPh0MywwxzKfyvFnEr-FKMr9UgOExqrZWY1gFeDot5S9rJhErLrZWAI8cn2tUuOGMg1LozqOxJIOyHsAERJ4W7jJQ5dWosbzdsiSeXAauX4fB_VWbAxmxNhreT1mJAF5Owx1OLJGYTFsmKqSI4HsGvlVpwQcClh8ZIBmWiLwUstOO401BGBsh2lbyWUmRGQCgSymgMP6IuQ7hAnWAp7CyO92VWNfXMV3ynevpq4a8MzDsgXnFisMWx_7w1vsdnSvNfo8E', + }, }, 'ML-KEM-1024': { privateUsages: ['decapsulateBits', 'decapsulateKey'], @@ -72,5 +84,11 @@ var keyData = { 251, 65, 39, 3, 101, 145, 174, 21, 195, 144, 234, 14, 158, 32, 49, 57, 142, 113, ]), + jwk: { + priv: 'mHWy5rWG9IGnfaYItV_O0ohw230GdnpTJvQ5zTJiOy7oQhGRzPyiuXR08hyEwftBJwNlka4Vw5DqDp4gMTmOcQ', + kty: 'AKP', + alg: 'ML-KEM-1024', + pub: 'IUe4I2Yu3_Ix1zZYFhRrTaBXm9KI8sZbdfbBXtsk8yV5ZBa0HYip0qHDrSEniEVDDngDv9Nhibg-OKdEgFy2NEaVa4WbHQxsVrUO11sV5LZ3H4Ww7CNG_3Ye-4h-vRw0B8eYJSLFGouhMRs8Q4q0ZhVp1wI8fIOoM-cXxypf6Kw_hLZD2TCkA3dN8hxk_tC5o5aZ8bFw7UyFJhgjmcEvzaYV6VqzokdodPty1jWOmguRXmydfvE1RjUHodUFOIhSzwuH3BgYlVk2Xcp3IPyyjnSNLBwLKEg4LlANCUvENPY6EJNgp-toE9wqk9ao_0t5HPpaqDAU0frBe9MhAHuvH3DHWlukxSRy7rmqC2tra9XCbTxp9_QRrCEs8Aa956fFmjN_z_EAGEdS7QqIxfi414ECFAWcTpGmC4y-VHSUC3UUGjJW1eNHzLOrk6g0dgqNFwF2h9h56NKddiQ3S2JUSTmAaiQj5FKmswNW61Ie24Yo4qcQOGKg9ioKGPmPyZDJ5Xwgchh6vsqu7-cOWZiRd8xAjvCrf6oiDXEjs7i8faEZqJAkVmqbfqyy4CW5astIffZYnBxsckuqHylk6VyrE8GalasJ65aY2ft0femcNJh5xSTBsmTHJtyj2hU1FcyNndgrLbdpcQobXdmyX0i3nsN6HXwKv_iAV6B6jCaTJVgLINwr0PW9XHaKLXKSw9QgmGc47qzCjiiEBZVrzxKz0ohDzlcGCvRapgDGakg1nBW54eRbuJi7kRynkSAaK2FKkuJ60Exw5PgGpBRRrFGI_KVbKMw7GkvOP6c3F0obH0iuk1TOsim92KJIixrOK2KECCrMODGAtDOVlxQxhDG57UGAp2KeGIwzhknHMCO081UV4Iy9dPctfVs6DlUwjHiMb9c5deJX7vwSVHxXqSUL52N-QNpBMXEX3TlsHafGewhSQhk3SfRNVxLAqqpWxyRxAKMKKsBEFDi1LsQGlQdF_RwjtNHMBOwJEeSy3lSAiLmInLVQ9-xUYXWZJmyY8jCwU4OUrSg3v0ge0cAtglA3UwAX5DU4i2AOT8YPkuR3DseMsirFEEOc2pI7tCpy4YxdRyqY9XWRNYTCI2UAtAdIKaaslRMEhnZpI8VYBqUehNCvkPIY8GxuA_o3XlopD8g8gBlqqqqxqYNdrQVLiWjCZyajXOBwL8uWiMOSQWcmHztxlRJ3TjqDZSYlZ2RZzBWGy7O1bXsSPAcu64EyWPFYgWEHAwGUAOodl5F95acdk0C1ogE3aBSz-SGqWddCZUUE1YlZ8etr-Dt8_9y4IUaixqxDZcZcU0ZiaDBNiKQWkIl3yIvKF3YwUdgXeyBg3jkUDDyTE2vLFYtzH-Vl6aOBNJTHaKESEUFyo4FW6qkwU5bNuXJed6ygiLZES6MKXGkHZzxt0xN_-1GWMcwa5Rxfq7V6KSh64sWSmEyXtXBnhesfUuMXfMFXFNzCanuIhIdabku6uPutNSOuRgW-5aajfVV_Bbtgx9eibpZHfYw42eqcJUdtsZEYgRUkzVgYxtFn6WyhQqhDeXGry4lUJrMGUWmmr5yGvbW8u1GFS7LITjODYvms1kKtRpEdNCIJICwlyQwhn_eVsYc-OWMG71iNeqELTgRj1olWgJpXenFMe0K8ctyMnSNY6LQOd-tTtUx8cQnKmxA5ThtrhYtS8JS0iDmUUcikf8aJxxZs7hh9HQp9jttI4PsBo8dKdMRHJQLEW1cY8nGG21C1nRzKKeet8hNA2va3T-xznbdzkEJ1FNae1fMWhZUO1pRAGUfB_ARPnAeNRRFn-XmLCwtKtRy_GrpHsmhlVZdvioJVZGoB9JusQamo7PhciUmTbpx69rc55nF7EKjOyHFaH3O5GCmulSLJK0fKpTG2d0KWlKDK13IK4PMvVpd0kegv-VSyF3as7QwEuVQrdJJOHZi5MZMsF4vISwgGdcN5JBdFdCXAvgmOqlpR0Li6HCUoD0GXs7KPZwCADLk1lynACrq_HodeXadFvpORW0ok07UF58Swb-SbdatO2dQjfwWB0dC2a9JINjSS6WGfrIKrOriqP-5wjVKdo5w1493fQtp3aXqOEio', + }, }, }; diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey.https.any.js b/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey.https.any.js index 6a5cc8d4724b57..3b78bab4e74132 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/ec_importKey.https.any.js @@ -220,44 +220,6 @@ // Helper methods follow: - // Are two array buffers the same? - function equalBuffers(a, b) { - if (a.byteLength !== b.byteLength) { - return false; - } - - var aBytes = new Uint8Array(a); - var bBytes = new Uint8Array(b); - - for (var i=0; i { + var cryptoKey = await crypto.subtle.generateKey( + generateKeyAlgorithm, true, generateKeyUsages); + const keyExported = + await crypto.subtle.exportKey(exportFormat, cryptoKey); + + const {key} = structuredClone({key: cryptoKey}); + const newKeyExported = + await crypto.subtle.exportKey(exportFormat, key); + assert_true(equalBuffers(keyExported, newKeyExported)); + }, 'serialization test ' + objectToString(generateKeyAlgorithm)); + }; + + function testCryptoKeyPairSerialization( + generateKeyAlgorithm, generateKeyUsages, publicExportFormat, + privateExportFormat) { + promise_test(async t => { + var keyPair = await crypto.subtle.generateKey( + generateKeyAlgorithm, true, generateKeyUsages); + const publicKeyExported = + await crypto.subtle.exportKey(publicExportFormat, keyPair.publicKey); + const privateKeyExported = await crypto.subtle.exportKey( + privateExportFormat, keyPair.privateKey); + + const {publicKey, privateKey} = structuredClone( + {publicKey: keyPair.publicKey, privateKey: keyPair.privateKey}); + const newPublicKeyExported = + await crypto.subtle.exportKey(publicExportFormat, publicKey); + assert_true(equalBuffers(publicKeyExported, newPublicKeyExported)); + const newPrivateKeyExported = await crypto.subtle.exportKey( + privateExportFormat, privateKey); + assert_true(equalBuffers(privateKeyExported, newPrivateKeyExported)); + }, 'serialization test ' + objectToString(generateKeyAlgorithm)); + }; + + vectors.forEach(function(vector) { + if (vector.resultType === 'CryptoKey') { + allAlgorithmSpecifiersFor(vector.name) + .forEach(function(generateKeyAlgorithm) { + testCryptoKeySerialization( + generateKeyAlgorithm, vector.usages, vector.exportFormat); + }); + } else { + allAlgorithmSpecifiersFor(vector.name) + .forEach(function(generateKeyAlgorithm) { + testCryptoKeyPairSerialization( + generateKeyAlgorithm, vector.usages, vector.publicFormat, + vector.privateFormat); + }); + } + }); +} diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js new file mode 100644 index 00000000000000..618bf29ebfe206 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/x25519.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'X25519', + resultType: 'CryptoKeyPair', + usages: ['deriveKey', 'deriveBits'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js new file mode 100644 index 00000000000000..cafc00144a3ce7 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/serialization/x448.tentative.https.any.js @@ -0,0 +1,12 @@ +// META: title=WebCryptoAPI: CryptoKey serialization +// META: script=../util/helpers.js +// META: script=serialization.js +run_test([ + { + name: 'X448', + resultType: 'CryptoKeyPair', + usages: ['deriveKey', 'deriveBits'], + publicFormat: 'raw', + privateFormat: 'pkcs8' + }, +]); diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.https.any.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.https.any.js index 9764cc33540c08..3f1e2e5ea9d8a7 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.https.any.js @@ -1,4 +1,5 @@ // META: title=WebCryptoAPI: sign() and verify() Using ECDSA +// META: script=../util/helpers.js // META: script=ecdsa_vectors.js // META: script=ecdsa.js // META: timeout=long diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.js index b2e0bf606b5fee..cc13b1bd9c3cae 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/ecdsa.js @@ -654,34 +654,5 @@ function run_test() { return Promise.all([publicPromise, privatePromise]); } - // Returns a copy of the sourceBuffer it is sent. - function copyBuffer(sourceBuffer) { - var source = new Uint8Array(sourceBuffer); - var copy = new Uint8Array(sourceBuffer.byteLength) - - for (var i=0; i> (8 - remainder) === bBytes[length] >> (8 - remainder); + } + + return true; +} + +// Returns a copy of the sourceBuffer it is sent. +function copyBuffer(sourceBuffer) { + var source = new Uint8Array(sourceBuffer); + var copy = new Uint8Array(sourceBuffer.byteLength) + + for (var i=0; i getAttributeNames(); DOMString? getAttribute(DOMString qualifiedName); DOMString? getAttributeNS(DOMString? namespace, DOMString localName); - [CEReactions] undefined setAttribute(DOMString qualifiedName, DOMString value); - [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, DOMString value); + [CEReactions] undefined setAttribute(DOMString qualifiedName, (TrustedType or DOMString) value); + [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, (TrustedType or DOMString) value); [CEReactions] undefined removeAttribute(DOMString qualifiedName); [CEReactions] undefined removeAttributeNS(DOMString? namespace, DOMString localName); [CEReactions] boolean toggleAttribute(DOMString qualifiedName, optional boolean force); @@ -412,7 +412,7 @@ dictionary ShadowRootInit { SlotAssignmentMode slotAssignment = "named"; boolean clonable = false; boolean serializable = false; - CustomElementRegistry customElementRegistry; + CustomElementRegistry? customElementRegistry; }; [Exposed=Window, diff --git a/test/fixtures/wpt/interfaces/html.idl b/test/fixtures/wpt/interfaces/html.idl index 9c84e6a67efa4f..567e5a79a36ab8 100644 --- a/test/fixtures/wpt/interfaces/html.idl +++ b/test/fixtures/wpt/interfaces/html.idl @@ -110,21 +110,21 @@ interface HTMLElement : Element { [HTMLConstructor] constructor(); // metadata attributes - [CEReactions] attribute DOMString title; - [CEReactions] attribute DOMString lang; + [CEReactions, Reflect] attribute DOMString title; + [CEReactions, Reflect] attribute DOMString lang; [CEReactions] attribute boolean translate; [CEReactions] attribute DOMString dir; // user interaction [CEReactions] attribute (boolean or unrestricted double or DOMString)? hidden; - [CEReactions] attribute boolean inert; + [CEReactions, Reflect] attribute boolean inert; undefined click(); - [CEReactions] attribute DOMString accessKey; + [CEReactions, Reflect] attribute DOMString accessKey; readonly attribute DOMString accessKeyLabel; [CEReactions] attribute boolean draggable; [CEReactions] attribute boolean spellcheck; - [CEReactions] attribute DOMString writingSuggestions; - [CEReactions] attribute DOMString autocapitalize; + [CEReactions, ReflectSetter] attribute DOMString writingSuggestions; + [CEReactions, ReflectSetter] attribute DOMString autocapitalize; [CEReactions] attribute boolean autocorrect; [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerText; @@ -137,6 +137,9 @@ interface HTMLElement : Element { undefined hidePopover(); boolean togglePopover(optional (TogglePopoverOptions or boolean) options = {}); [CEReactions] attribute DOMString? popover; + + [CEReactions, Reflect, ReflectRange=(0, 8)] attribute unsigned long headingOffset; + [CEReactions, Reflect] attribute boolean headingReset; }; dictionary ShowPopoverOptions { @@ -160,8 +163,8 @@ interface mixin HTMLOrSVGElement { [SameObject] readonly attribute DOMStringMap dataset; attribute DOMString nonce; // intentionally no [CEReactions] - [CEReactions] attribute boolean autofocus; - [CEReactions] attribute long tabIndex; + [CEReactions, Reflect] attribute boolean autofocus; + [CEReactions, ReflectSetter] attribute long tabIndex; undefined focus(optional FocusOptions options = {}); undefined blur(); }; @@ -197,29 +200,29 @@ interface HTMLTitleElement : HTMLElement { interface HTMLBaseElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString href; - [CEReactions] attribute DOMString target; + [CEReactions, ReflectSetter] attribute USVString href; + [CEReactions, Reflect] attribute DOMString target; }; [Exposed=Window] interface HTMLLinkElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString href; + [CEReactions, ReflectURL] attribute USVString href; [CEReactions] attribute DOMString? crossOrigin; - [CEReactions] attribute DOMString rel; + [CEReactions, Reflect] attribute DOMString rel; [CEReactions] attribute DOMString as; - [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; - [CEReactions] attribute DOMString media; - [CEReactions] attribute DOMString integrity; - [CEReactions] attribute DOMString hreflang; - [CEReactions] attribute DOMString type; - [SameObject, PutForwards=value] readonly attribute DOMTokenList sizes; - [CEReactions] attribute USVString imageSrcset; - [CEReactions] attribute DOMString imageSizes; + [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList; + [CEReactions, Reflect] attribute DOMString media; + [CEReactions, Reflect] attribute DOMString integrity; + [CEReactions, Reflect] attribute DOMString hreflang; + [CEReactions, Reflect] attribute DOMString type; + [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList sizes; + [CEReactions, Reflect] attribute USVString imageSrcset; + [CEReactions, Reflect] attribute DOMString imageSizes; [CEReactions] attribute DOMString referrerPolicy; - [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking; - [CEReactions] attribute boolean disabled; + [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking; + [CEReactions, Reflect] attribute boolean disabled; [CEReactions] attribute DOMString fetchPriority; // also has obsolete members @@ -230,10 +233,10 @@ HTMLLinkElement includes LinkStyle; interface HTMLMetaElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString httpEquiv; - [CEReactions] attribute DOMString content; - [CEReactions] attribute DOMString media; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect="http-equiv"] attribute DOMString httpEquiv; + [CEReactions, Reflect] attribute DOMString content; + [CEReactions, Reflect] attribute DOMString media; // also has obsolete members }; @@ -243,8 +246,8 @@ interface HTMLStyleElement : HTMLElement { [HTMLConstructor] constructor(); attribute boolean disabled; - [CEReactions] attribute DOMString media; - [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking; + [CEReactions, Reflect] attribute DOMString media; + [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking; // also has obsolete members }; @@ -291,16 +294,16 @@ interface HTMLPreElement : HTMLElement { interface HTMLQuoteElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString cite; + [CEReactions, ReflectURL] attribute USVString cite; }; [Exposed=Window] interface HTMLOListElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean reversed; - [CEReactions] attribute long start; - [CEReactions] attribute DOMString type; + [CEReactions, Reflect] attribute boolean reversed; + [CEReactions, Reflect, ReflectDefault=1] attribute long start; + [CEReactions, Reflect] attribute DOMString type; // also has obsolete members }; @@ -323,7 +326,7 @@ interface HTMLMenuElement : HTMLElement { interface HTMLLIElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute long value; + [CEReactions, Reflect] attribute long value; // also has obsolete members }; @@ -346,13 +349,13 @@ interface HTMLDivElement : HTMLElement { interface HTMLAnchorElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString target; - [CEReactions] attribute DOMString download; - [CEReactions] attribute USVString ping; - [CEReactions] attribute DOMString rel; - [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; - [CEReactions] attribute DOMString hreflang; - [CEReactions] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString target; + [CEReactions, Reflect] attribute DOMString download; + [CEReactions, Reflect] attribute USVString ping; + [CEReactions, Reflect] attribute DOMString rel; + [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList; + [CEReactions, Reflect] attribute DOMString hreflang; + [CEReactions, Reflect] attribute DOMString type; [CEReactions] attribute DOMString text; @@ -360,20 +363,21 @@ interface HTMLAnchorElement : HTMLElement { // also has obsolete members }; +HTMLAnchorElement includes HyperlinkElementUtils; HTMLAnchorElement includes HTMLHyperlinkElementUtils; [Exposed=Window] interface HTMLDataElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString value; + [CEReactions, Reflect] attribute DOMString value; }; [Exposed=Window] interface HTMLTimeElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString dateTime; + [CEReactions, Reflect] attribute DOMString dateTime; }; [Exposed=Window] @@ -388,8 +392,7 @@ interface HTMLBRElement : HTMLElement { // also has obsolete members }; -interface mixin HTMLHyperlinkElementUtils { - [CEReactions] stringifier attribute USVString href; +interface mixin HyperlinkElementUtils { readonly attribute USVString origin; [CEReactions] attribute USVString protocol; [CEReactions] attribute USVString username; @@ -402,12 +405,16 @@ interface mixin HTMLHyperlinkElementUtils { [CEReactions] attribute USVString hash; }; +interface mixin HTMLHyperlinkElementUtils { + [CEReactions, ReflectSetter] stringifier attribute USVString href; +}; + [Exposed=Window] interface HTMLModElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString cite; - [CEReactions] attribute DOMString dateTime; + [CEReactions, ReflectURL] attribute USVString cite; + [CEReactions, Reflect] attribute DOMString dateTime; }; [Exposed=Window] @@ -419,13 +426,13 @@ interface HTMLPictureElement : HTMLElement { interface HTMLSourceElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString type; - [CEReactions] attribute USVString srcset; - [CEReactions] attribute DOMString sizes; - [CEReactions] attribute DOMString media; - [CEReactions] attribute unsigned long width; - [CEReactions] attribute unsigned long height; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute DOMString type; + [CEReactions, Reflect] attribute USVString srcset; + [CEReactions, Reflect] attribute DOMString sizes; + [CEReactions, Reflect] attribute DOMString media; + [CEReactions, Reflect] attribute unsigned long width; + [CEReactions, Reflect] attribute unsigned long height; }; [Exposed=Window, @@ -433,15 +440,15 @@ interface HTMLSourceElement : HTMLElement { interface HTMLImageElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString alt; - [CEReactions] attribute USVString src; - [CEReactions] attribute USVString srcset; - [CEReactions] attribute DOMString sizes; + [CEReactions, Reflect] attribute DOMString alt; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute USVString srcset; + [CEReactions, Reflect] attribute DOMString sizes; [CEReactions] attribute DOMString? crossOrigin; - [CEReactions] attribute DOMString useMap; - [CEReactions] attribute boolean isMap; - [CEReactions] attribute unsigned long width; - [CEReactions] attribute unsigned long height; + [CEReactions, Reflect] attribute DOMString useMap; + [CEReactions, Reflect] attribute boolean isMap; + [CEReactions, ReflectSetter] attribute unsigned long width; + [CEReactions, ReflectSetter] attribute unsigned long height; readonly attribute unsigned long naturalWidth; readonly attribute unsigned long naturalHeight; readonly attribute boolean complete; @@ -460,14 +467,14 @@ interface HTMLImageElement : HTMLElement { interface HTMLIFrameElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString src; + [CEReactions, ReflectURL] attribute USVString src; [CEReactions] attribute (TrustedHTML or DOMString) srcdoc; - [CEReactions] attribute DOMString name; - [SameObject, PutForwards=value] readonly attribute DOMTokenList sandbox; - [CEReactions] attribute DOMString allow; - [CEReactions] attribute boolean allowFullscreen; - [CEReactions] attribute DOMString width; - [CEReactions] attribute DOMString height; + [CEReactions, Reflect] attribute DOMString name; + [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList sandbox; + [CEReactions, Reflect] attribute DOMString allow; + [CEReactions, Reflect] attribute boolean allowFullscreen; + [CEReactions, Reflect] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString height; [CEReactions] attribute DOMString referrerPolicy; [CEReactions] attribute DOMString loading; readonly attribute Document? contentDocument; @@ -481,10 +488,10 @@ interface HTMLIFrameElement : HTMLElement { interface HTMLEmbedElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString type; - [CEReactions] attribute DOMString width; - [CEReactions] attribute DOMString height; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString height; Document? getSVGDocument(); // also has obsolete members @@ -494,12 +501,12 @@ interface HTMLEmbedElement : HTMLElement { interface HTMLObjectElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString data; - [CEReactions] attribute DOMString type; - [CEReactions] attribute DOMString name; + [CEReactions, ReflectURL] attribute USVString data; + [CEReactions, Reflect] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString name; readonly attribute HTMLFormElement? form; - [CEReactions] attribute DOMString width; - [CEReactions] attribute DOMString height; + [CEReactions, Reflect] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString height; readonly attribute Document? contentDocument; readonly attribute WindowProxy? contentWindow; Document? getSVGDocument(); @@ -518,12 +525,12 @@ interface HTMLObjectElement : HTMLElement { interface HTMLVideoElement : HTMLMediaElement { [HTMLConstructor] constructor(); - [CEReactions] attribute unsigned long width; - [CEReactions] attribute unsigned long height; + [CEReactions, Reflect] attribute unsigned long width; + [CEReactions, Reflect] attribute unsigned long height; readonly attribute unsigned long videoWidth; readonly attribute unsigned long videoHeight; - [CEReactions] attribute USVString poster; - [CEReactions] attribute boolean playsInline; + [CEReactions, ReflectURL] attribute USVString poster; + [CEReactions, Reflect] attribute boolean playsInline; }; [Exposed=Window, @@ -537,10 +544,10 @@ interface HTMLTrackElement : HTMLElement { [HTMLConstructor] constructor(); [CEReactions] attribute DOMString kind; - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString srclang; - [CEReactions] attribute DOMString label; - [CEReactions] attribute boolean default; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute DOMString srclang; + [CEReactions, Reflect] attribute DOMString label; + [CEReactions, Reflect] attribute boolean default; const unsigned short NONE = 0; const unsigned short LOADING = 1; @@ -561,7 +568,7 @@ interface HTMLMediaElement : HTMLElement { readonly attribute MediaError? error; // network state - [CEReactions] attribute USVString src; + [CEReactions, ReflectURL] attribute USVString src; attribute MediaProvider? srcObject; readonly attribute USVString currentSrc; [CEReactions] attribute DOMString? crossOrigin; @@ -596,16 +603,16 @@ interface HTMLMediaElement : HTMLElement { readonly attribute TimeRanges played; readonly attribute TimeRanges seekable; readonly attribute boolean ended; - [CEReactions] attribute boolean autoplay; - [CEReactions] attribute boolean loop; + [CEReactions, Reflect] attribute boolean autoplay; + [CEReactions, Reflect] attribute boolean loop; Promise play(); undefined pause(); // controls - [CEReactions] attribute boolean controls; + [CEReactions, Reflect] attribute boolean controls; attribute double volume; attribute boolean muted; - [CEReactions] attribute boolean defaultMuted; + [CEReactions, Reflect="muted"] attribute boolean defaultMuted; // tracks [SameObject] readonly attribute AudioTrackList audioTracks; @@ -742,7 +749,7 @@ dictionary TrackEventInit : EventInit { interface HTMLMapElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString name; [SameObject] readonly attribute HTMLCollection areas; }; @@ -750,18 +757,19 @@ interface HTMLMapElement : HTMLElement { interface HTMLAreaElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString alt; - [CEReactions] attribute DOMString coords; - [CEReactions] attribute DOMString shape; - [CEReactions] attribute DOMString target; - [CEReactions] attribute DOMString download; - [CEReactions] attribute USVString ping; - [CEReactions] attribute DOMString rel; - [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; + [CEReactions, Reflect] attribute DOMString alt; + [CEReactions, Reflect] attribute DOMString coords; + [CEReactions, Reflect] attribute DOMString shape; + [CEReactions, Reflect] attribute DOMString target; + [CEReactions, Reflect] attribute DOMString download; + [CEReactions, Reflect] attribute USVString ping; + [CEReactions, Reflect] attribute DOMString rel; + [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList; [CEReactions] attribute DOMString referrerPolicy; // also has obsolete members }; +HTMLAreaElement includes HyperlinkElementUtils; HTMLAreaElement includes HTMLHyperlinkElementUtils; [Exposed=Window] @@ -801,7 +809,7 @@ interface HTMLTableCaptionElement : HTMLElement { interface HTMLTableColElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute unsigned long span; + [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(1, 1000)] attribute unsigned long span; // also has obsolete members }; @@ -834,13 +842,13 @@ interface HTMLTableRowElement : HTMLElement { interface HTMLTableCellElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute unsigned long colSpan; - [CEReactions] attribute unsigned long rowSpan; - [CEReactions] attribute DOMString headers; + [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(1, 1000)] attribute unsigned long colSpan; + [CEReactions, Reflect, ReflectDefault=1, ReflectRange=(0, 65534)] attribute unsigned long rowSpan; + [CEReactions, Reflect] attribute DOMString headers; readonly attribute long cellIndex; [CEReactions] attribute DOMString scope; // only conforming for th elements - [CEReactions] attribute DOMString abbr; // only conforming for th elements + [CEReactions, Reflect] attribute DOMString abbr; // only conforming for th elements // also has obsolete members }; @@ -851,17 +859,17 @@ interface HTMLTableCellElement : HTMLElement { interface HTMLFormElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString acceptCharset; - [CEReactions] attribute USVString action; + [CEReactions, Reflect="accept-charset"] attribute DOMString acceptCharset; + [CEReactions, ReflectSetter] attribute USVString action; [CEReactions] attribute DOMString autocomplete; [CEReactions] attribute DOMString enctype; [CEReactions] attribute DOMString encoding; [CEReactions] attribute DOMString method; - [CEReactions] attribute DOMString name; - [CEReactions] attribute boolean noValidate; - [CEReactions] attribute DOMString target; - [CEReactions] attribute DOMString rel; - [SameObject, PutForwards=value] readonly attribute DOMTokenList relList; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute boolean noValidate; + [CEReactions, Reflect] attribute DOMString target; + [CEReactions, Reflect] attribute DOMString rel; + [SameObject, PutForwards=value, Reflect="rel"] readonly attribute DOMTokenList relList; [SameObject] readonly attribute HTMLFormControlsCollection elements; readonly attribute unsigned long length; @@ -880,7 +888,7 @@ interface HTMLLabelElement : HTMLElement { [HTMLConstructor] constructor(); readonly attribute HTMLFormElement? form; - [CEReactions] attribute DOMString htmlFor; + [CEReactions, Reflect="for"] attribute DOMString htmlFor; readonly attribute HTMLElement? control; }; @@ -888,44 +896,44 @@ interface HTMLLabelElement : HTMLElement { interface HTMLInputElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString accept; - [CEReactions] attribute boolean alpha; - [CEReactions] attribute DOMString alt; - [CEReactions] attribute DOMString autocomplete; - [CEReactions] attribute boolean defaultChecked; + [CEReactions, Reflect] attribute DOMString accept; + [CEReactions, Reflect] attribute boolean alpha; + [CEReactions, Reflect] attribute DOMString alt; + [CEReactions, ReflectSetter] attribute DOMString autocomplete; + [CEReactions, Reflect="checked"] attribute boolean defaultChecked; attribute boolean checked; [CEReactions] attribute DOMString colorSpace; - [CEReactions] attribute DOMString dirName; - [CEReactions] attribute boolean disabled; + [CEReactions, Reflect] attribute DOMString dirName; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; attribute FileList? files; - [CEReactions] attribute USVString formAction; + [CEReactions, ReflectSetter] attribute USVString formAction; [CEReactions] attribute DOMString formEnctype; [CEReactions] attribute DOMString formMethod; - [CEReactions] attribute boolean formNoValidate; - [CEReactions] attribute DOMString formTarget; - [CEReactions] attribute unsigned long height; + [CEReactions, Reflect] attribute boolean formNoValidate; + [CEReactions, Reflect] attribute DOMString formTarget; + [CEReactions, ReflectSetter] attribute unsigned long height; attribute boolean indeterminate; readonly attribute HTMLDataListElement? list; - [CEReactions] attribute DOMString max; - [CEReactions] attribute long maxLength; - [CEReactions] attribute DOMString min; - [CEReactions] attribute long minLength; - [CEReactions] attribute boolean multiple; - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString pattern; - [CEReactions] attribute DOMString placeholder; - [CEReactions] attribute boolean readOnly; - [CEReactions] attribute boolean required; - [CEReactions] attribute unsigned long size; - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString step; + [CEReactions, Reflect] attribute DOMString max; + [CEReactions, ReflectNonNegative] attribute long maxLength; + [CEReactions, Reflect] attribute DOMString min; + [CEReactions, ReflectNonNegative] attribute long minLength; + [CEReactions, Reflect] attribute boolean multiple; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString pattern; + [CEReactions, Reflect] attribute DOMString placeholder; + [CEReactions, Reflect] attribute boolean readOnly; + [CEReactions, Reflect] attribute boolean required; + [CEReactions, Reflect] attribute unsigned long size; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute DOMString step; [CEReactions] attribute DOMString type; - [CEReactions] attribute DOMString defaultValue; + [CEReactions, Reflect="value"] attribute DOMString defaultValue; [CEReactions] attribute [LegacyNullToEmptyString] DOMString value; attribute object? valueAsDate; attribute unrestricted double valueAsNumber; - [CEReactions] attribute unsigned long width; + [CEReactions, ReflectSetter] attribute unsigned long width; undefined stepUp(optional long n = 1); undefined stepDown(optional long n = 1); @@ -951,24 +959,24 @@ interface HTMLInputElement : HTMLElement { // also has obsolete members }; -HTMLInputElement includes PopoverInvokerElement; +HTMLInputElement includes PopoverTargetAttributes; [Exposed=Window] interface HTMLButtonElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString command; - [CEReactions] attribute Element? commandForElement; - [CEReactions] attribute boolean disabled; + [CEReactions, ReflectSetter] attribute DOMString command; + [CEReactions, Reflect] attribute Element? commandForElement; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - [CEReactions] attribute USVString formAction; + [CEReactions, ReflectSetter] attribute USVString formAction; [CEReactions] attribute DOMString formEnctype; [CEReactions] attribute DOMString formMethod; - [CEReactions] attribute boolean formNoValidate; - [CEReactions] attribute DOMString formTarget; - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString type; - [CEReactions] attribute DOMString value; + [CEReactions, Reflect] attribute boolean formNoValidate; + [CEReactions, Reflect] attribute DOMString formTarget; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, ReflectSetter] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString value; readonly attribute boolean willValidate; readonly attribute ValidityState validity; @@ -979,19 +987,19 @@ interface HTMLButtonElement : HTMLElement { readonly attribute NodeList labels; }; -HTMLButtonElement includes PopoverInvokerElement; +HTMLButtonElement includes PopoverTargetAttributes; [Exposed=Window] interface HTMLSelectElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString autocomplete; - [CEReactions] attribute boolean disabled; + [CEReactions, ReflectSetter] attribute DOMString autocomplete; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - [CEReactions] attribute boolean multiple; - [CEReactions] attribute DOMString name; - [CEReactions] attribute boolean required; - [CEReactions] attribute unsigned long size; + [CEReactions, Reflect] attribute boolean multiple; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute boolean required; + [CEReactions, Reflect, ReflectDefault=0] attribute unsigned long size; readonly attribute DOMString type; @@ -1031,8 +1039,8 @@ interface HTMLDataListElement : HTMLElement { interface HTMLOptGroupElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean disabled; - [CEReactions] attribute DOMString label; + [CEReactions, Reflect] attribute boolean disabled; + [CEReactions, Reflect] attribute DOMString label; }; [Exposed=Window, @@ -1040,12 +1048,12 @@ interface HTMLOptGroupElement : HTMLElement { interface HTMLOptionElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean disabled; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - [CEReactions] attribute DOMString label; - [CEReactions] attribute boolean defaultSelected; + [CEReactions, ReflectSetter] attribute DOMString label; + [CEReactions, Reflect="selected"] attribute boolean defaultSelected; attribute boolean selected; - [CEReactions] attribute DOMString value; + [CEReactions, ReflectSetter] attribute DOMString value; [CEReactions] attribute DOMString text; readonly attribute long index; @@ -1055,19 +1063,19 @@ interface HTMLOptionElement : HTMLElement { interface HTMLTextAreaElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString autocomplete; - [CEReactions] attribute unsigned long cols; - [CEReactions] attribute DOMString dirName; - [CEReactions] attribute boolean disabled; + [CEReactions, ReflectSetter] attribute DOMString autocomplete; + [CEReactions, ReflectPositiveWithFallback, ReflectDefault=20] attribute unsigned long cols; + [CEReactions, Reflect] attribute DOMString dirName; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - [CEReactions] attribute long maxLength; - [CEReactions] attribute long minLength; - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString placeholder; - [CEReactions] attribute boolean readOnly; - [CEReactions] attribute boolean required; - [CEReactions] attribute unsigned long rows; - [CEReactions] attribute DOMString wrap; + [CEReactions, ReflectNonNegative] attribute long maxLength; + [CEReactions, ReflectNonNegative] attribute long minLength; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString placeholder; + [CEReactions, Reflect] attribute boolean readOnly; + [CEReactions, Reflect] attribute boolean required; + [CEReactions, ReflectPositiveWithFallback, ReflectDefault=2] attribute unsigned long rows; + [CEReactions, Reflect] attribute DOMString wrap; readonly attribute DOMString type; [CEReactions] attribute DOMString defaultValue; @@ -1096,9 +1104,9 @@ interface HTMLTextAreaElement : HTMLElement { interface HTMLOutputElement : HTMLElement { [HTMLConstructor] constructor(); - [SameObject, PutForwards=value] readonly attribute DOMTokenList htmlFor; + [SameObject, PutForwards=value, Reflect="for"] readonly attribute DOMTokenList htmlFor; readonly attribute HTMLFormElement? form; - [CEReactions] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString name; readonly attribute DOMString type; [CEReactions] attribute DOMString defaultValue; @@ -1118,8 +1126,8 @@ interface HTMLOutputElement : HTMLElement { interface HTMLProgressElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute double value; - [CEReactions] attribute double max; + [CEReactions, ReflectSetter] attribute double value; + [CEReactions, ReflectPositive, ReflectDefault=1.0] attribute double max; readonly attribute double position; readonly attribute NodeList labels; }; @@ -1128,12 +1136,12 @@ interface HTMLProgressElement : HTMLElement { interface HTMLMeterElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute double value; - [CEReactions] attribute double min; - [CEReactions] attribute double max; - [CEReactions] attribute double low; - [CEReactions] attribute double high; - [CEReactions] attribute double optimum; + [CEReactions, ReflectSetter] attribute double value; + [CEReactions, ReflectSetter] attribute double min; + [CEReactions, ReflectSetter] attribute double max; + [CEReactions, ReflectSetter] attribute double low; + [CEReactions, ReflectSetter] attribute double high; + [CEReactions, ReflectSetter] attribute double optimum; readonly attribute NodeList labels; }; @@ -1141,9 +1149,9 @@ interface HTMLMeterElement : HTMLElement { interface HTMLFieldSetElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean disabled; + [CEReactions, Reflect] attribute boolean disabled; readonly attribute HTMLFormElement? form; - [CEReactions] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString name; readonly attribute DOMString type; @@ -1166,6 +1174,11 @@ interface HTMLLegendElement : HTMLElement { // also has obsolete members }; +[Exposed=Window] +interface HTMLSelectedContentElement : HTMLElement { + [HTMLConstructor] constructor(); +}; + enum SelectionMode { "select", "start", @@ -1214,17 +1227,17 @@ dictionary FormDataEventInit : EventInit { interface HTMLDetailsElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; - [CEReactions] attribute boolean open; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute boolean open; }; [Exposed=Window] interface HTMLDialogElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean open; + [CEReactions, Reflect] attribute boolean open; attribute DOMString returnValue; - [CEReactions] attribute DOMString closedBy; + [CEReactions, ReflectSetter] attribute DOMString closedBy; [CEReactions] undefined show(); [CEReactions] undefined showModal(); [CEReactions] undefined close(optional DOMString returnValue); @@ -1235,18 +1248,19 @@ interface HTMLDialogElement : HTMLElement { interface HTMLScriptElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString type; - [CEReactions] attribute boolean noModule; + [CEReactions, Reflect] attribute DOMString type; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute boolean noModule; [CEReactions] attribute boolean async; - [CEReactions] attribute boolean defer; + [CEReactions, Reflect] attribute boolean defer; + [SameObject, PutForwards=value, Reflect] readonly attribute DOMTokenList blocking; [CEReactions] attribute DOMString? crossOrigin; - [CEReactions] attribute DOMString text; - [CEReactions] attribute DOMString integrity; [CEReactions] attribute DOMString referrerPolicy; - [SameObject, PutForwards=value] readonly attribute DOMTokenList blocking; + [CEReactions, Reflect] attribute DOMString integrity; [CEReactions] attribute DOMString fetchPriority; + [CEReactions] attribute DOMString text; + static boolean supports(DOMString type); // also has obsolete members @@ -1258,17 +1272,17 @@ interface HTMLTemplateElement : HTMLElement { readonly attribute DocumentFragment content; [CEReactions] attribute DOMString shadowRootMode; - [CEReactions] attribute boolean shadowRootDelegatesFocus; - [CEReactions] attribute boolean shadowRootClonable; - [CEReactions] attribute boolean shadowRootSerializable; - [CEReactions] attribute DOMString shadowRootCustomElementRegistry; + [CEReactions, Reflect] attribute boolean shadowRootDelegatesFocus; + [CEReactions, Reflect] attribute boolean shadowRootClonable; + [CEReactions, Reflect] attribute boolean shadowRootSerializable; + [CEReactions, Reflect] attribute DOMString shadowRootCustomElementRegistry; }; [Exposed=Window] interface HTMLSlotElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString name; sequence assignedNodes(optional AssignedNodesOptions options = {}); sequence assignedElements(optional AssignedNodesOptions options = {}); undefined assign((Element or Text)... nodes); @@ -1306,8 +1320,6 @@ typedef (HTMLOrSVGImageElement or OffscreenCanvas or VideoFrame) CanvasImageSource; -enum PredefinedColorSpace { "srgb", "display-p3" }; - enum CanvasColorType { "unorm8", "float16" }; enum CanvasFillRule { "nonzero", "evenodd" }; @@ -1606,6 +1618,8 @@ OffscreenCanvasRenderingContext2D includes CanvasPathDrawingStyles; OffscreenCanvasRenderingContext2D includes CanvasTextDrawingStyles; OffscreenCanvasRenderingContext2D includes CanvasPath; +enum PredefinedColorSpace { "srgb", "srgb-linear", "display-p3", "display-p3-linear" }; + [Exposed=Window] interface CustomElementRegistry { constructor(); @@ -1615,7 +1629,7 @@ interface CustomElementRegistry { DOMString? getName(CustomElementConstructor constructor); Promise whenDefined(DOMString name); [CEReactions] undefined upgrade(Node root); - undefined initialize(Node root); + [CEReactions] undefined initialize(Node root); }; callback CustomElementConstructor = HTMLElement (); @@ -1694,11 +1708,13 @@ interface ToggleEvent : Event { constructor(DOMString type, optional ToggleEventInit eventInitDict = {}); readonly attribute DOMString oldState; readonly attribute DOMString newState; + readonly attribute Element? source; }; dictionary ToggleEventInit : EventInit { DOMString oldState = ""; DOMString newState = ""; + Element? source = null; }; [Exposed=Window] @@ -1791,11 +1807,23 @@ dictionary DragEventInit : MouseEventInit { DataTransfer? dataTransfer = null; }; -interface mixin PopoverInvokerElement { - [CEReactions] attribute Element? popoverTargetElement; +interface mixin PopoverTargetAttributes { + [CEReactions, Reflect] attribute Element? popoverTargetElement; [CEReactions] attribute DOMString popoverTargetAction; }; +[Exposed=*] +interface Origin { + constructor(); + + static Origin from(any value); + + readonly attribute boolean opaque; + + boolean isSameOrigin(Origin other); + boolean isSameSite(Origin other); +}; + [Global=Window, Exposed=Window, LegacyUnenumerableNamedProperties] @@ -1807,7 +1835,7 @@ interface Window : EventTarget { attribute DOMString name; [PutForwards=href, LegacyUnforgeable] readonly attribute Location location; readonly attribute History history; - readonly attribute Navigation navigation; + [Replaceable] readonly attribute Navigation navigation; readonly attribute CustomElementRegistry customElements; [Replaceable] readonly attribute BarProp locationbar; [Replaceable] readonly attribute BarProp menubar; @@ -1881,7 +1909,7 @@ interface Location { // but see also additional creation steps and overridden in [LegacyUnforgeable] undefined replace(USVString url); [LegacyUnforgeable] undefined reload(); - [LegacyUnforgeable, SameObject] readonly attribute DOMStringList ancestorOrigins; + [LegacyUnforgeable] readonly attribute DOMStringList ancestorOrigins; }; enum ScrollRestoration { "auto", "manual" }; @@ -1974,6 +2002,8 @@ interface NavigationHistoryEntry : EventTarget { interface NavigationTransition { readonly attribute NavigationType navigationType; readonly attribute NavigationHistoryEntry from; + readonly attribute NavigationDestination to; + readonly attribute Promise committed; readonly attribute Promise finished; }; @@ -2019,6 +2049,7 @@ dictionary NavigateEventInit : EventInit { }; dictionary NavigationInterceptOptions { + NavigationPrecommitHandler precommitHandler; NavigationInterceptHandler handler; NavigationFocusReset focusReset; NavigationScrollBehavior scroll; @@ -2036,6 +2067,14 @@ enum NavigationScrollBehavior { callback NavigationInterceptHandler = Promise (); +[Exposed=Window] +interface NavigationPrecommitController { + undefined redirect(USVString url, optional NavigationNavigateOptions options = {}); + undefined addHandler(NavigationInterceptHandler handler); +}; + +callback NavigationPrecommitHandler = Promise (NavigationPrecommitController controller); + [Exposed=Window] interface NavigationDestination { readonly attribute USVString url; @@ -2649,9 +2688,9 @@ interface Worker : EventTarget { }; dictionary WorkerOptions { + DOMString name = ""; WorkerType type = "classic"; RequestCredentials credentials = "same-origin"; // credentials is only used if type is "module" - DOMString name = ""; }; enum WorkerType { "classic", "module" }; @@ -2661,12 +2700,16 @@ Worker includes MessageEventTarget; [Exposed=Window] interface SharedWorker : EventTarget { - constructor((TrustedScriptURL or USVString) scriptURL, optional (DOMString or WorkerOptions) options = {}); + constructor((TrustedScriptURL or USVString) scriptURL, optional (DOMString or SharedWorkerOptions) options = {}); readonly attribute MessagePort port; }; SharedWorker includes AbstractWorker; +dictionary SharedWorkerOptions : WorkerOptions { + boolean extendedLifetime = false; +}; + interface mixin NavigatorConcurrentHardware { readonly attribute unsigned long long hardwareConcurrency; }; @@ -2748,17 +2791,17 @@ dictionary StorageEventInit : EventInit { interface HTMLMarqueeElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString behavior; - [CEReactions] attribute DOMString bgColor; - [CEReactions] attribute DOMString direction; - [CEReactions] attribute DOMString height; - [CEReactions] attribute unsigned long hspace; + [CEReactions, Reflect] attribute DOMString behavior; + [CEReactions, Reflect] attribute DOMString bgColor; + [CEReactions, Reflect] attribute DOMString direction; + [CEReactions, Reflect] attribute DOMString height; + [CEReactions, Reflect] attribute unsigned long hspace; [CEReactions] attribute long loop; - [CEReactions] attribute unsigned long scrollAmount; - [CEReactions] attribute unsigned long scrollDelay; - [CEReactions] attribute boolean trueSpeed; - [CEReactions] attribute unsigned long vspace; - [CEReactions] attribute DOMString width; + [CEReactions, Reflect, ReflectDefault=6] attribute unsigned long scrollAmount; + [CEReactions, Reflect, ReflectDefault=85] attribute unsigned long scrollDelay; + [CEReactions, Reflect] attribute boolean trueSpeed; + [CEReactions, Reflect] attribute unsigned long vspace; + [CEReactions, Reflect] attribute DOMString width; undefined start(); undefined stop(); @@ -2768,8 +2811,8 @@ interface HTMLMarqueeElement : HTMLElement { interface HTMLFrameSetElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString cols; - [CEReactions] attribute DOMString rows; + [CEReactions, Reflect] attribute DOMString cols; + [CEReactions, Reflect] attribute DOMString rows; }; HTMLFrameSetElement includes WindowEventHandlers; @@ -2777,242 +2820,242 @@ HTMLFrameSetElement includes WindowEventHandlers; interface HTMLFrameElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString scrolling; - [CEReactions] attribute USVString src; - [CEReactions] attribute DOMString frameBorder; - [CEReactions] attribute USVString longDesc; - [CEReactions] attribute boolean noResize; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString scrolling; + [CEReactions, ReflectURL] attribute USVString src; + [CEReactions, Reflect] attribute DOMString frameBorder; + [CEReactions, ReflectURL] attribute USVString longDesc; + [CEReactions, Reflect] attribute boolean noResize; readonly attribute Document? contentDocument; readonly attribute WindowProxy? contentWindow; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginHeight; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginWidth; }; partial interface HTMLAnchorElement { - [CEReactions] attribute DOMString coords; - [CEReactions] attribute DOMString charset; - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString rev; - [CEReactions] attribute DOMString shape; + [CEReactions, Reflect] attribute DOMString coords; + [CEReactions, Reflect] attribute DOMString charset; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString rev; + [CEReactions, Reflect] attribute DOMString shape; }; partial interface HTMLAreaElement { - [CEReactions] attribute boolean noHref; + [CEReactions, Reflect] attribute boolean noHref; }; partial interface HTMLBodyElement { - [CEReactions] attribute [LegacyNullToEmptyString] DOMString text; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString link; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString vLink; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString aLink; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor; - [CEReactions] attribute DOMString background; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString text; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString link; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString vLink; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString aLink; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor; + [CEReactions, Reflect] attribute DOMString background; }; partial interface HTMLBRElement { - [CEReactions] attribute DOMString clear; + [CEReactions, Reflect] attribute DOMString clear; }; partial interface HTMLTableCaptionElement { - [CEReactions] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString align; }; partial interface HTMLTableColElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString ch; - [CEReactions] attribute DOMString chOff; - [CEReactions] attribute DOMString vAlign; - [CEReactions] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect="char"] attribute DOMString ch; + [CEReactions, Reflect="charoff"] attribute DOMString chOff; + [CEReactions, Reflect] attribute DOMString vAlign; + [CEReactions, Reflect] attribute DOMString width; }; [Exposed=Window] interface HTMLDirectoryElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute boolean compact; + [CEReactions, Reflect] attribute boolean compact; }; partial interface HTMLDivElement { - [CEReactions] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString align; }; partial interface HTMLDListElement { - [CEReactions] attribute boolean compact; + [CEReactions, Reflect] attribute boolean compact; }; partial interface HTMLEmbedElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString name; }; [Exposed=Window] interface HTMLFontElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute [LegacyNullToEmptyString] DOMString color; - [CEReactions] attribute DOMString face; - [CEReactions] attribute DOMString size; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString color; + [CEReactions, Reflect] attribute DOMString face; + [CEReactions, Reflect] attribute DOMString size; }; partial interface HTMLHeadingElement { - [CEReactions] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString align; }; partial interface HTMLHRElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString color; - [CEReactions] attribute boolean noShade; - [CEReactions] attribute DOMString size; - [CEReactions] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString color; + [CEReactions, Reflect] attribute boolean noShade; + [CEReactions, Reflect] attribute DOMString size; + [CEReactions, Reflect] attribute DOMString width; }; partial interface HTMLHtmlElement { - [CEReactions] attribute DOMString version; + [CEReactions, Reflect] attribute DOMString version; }; partial interface HTMLIFrameElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString scrolling; - [CEReactions] attribute DOMString frameBorder; - [CEReactions] attribute USVString longDesc; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString scrolling; + [CEReactions, Reflect] attribute DOMString frameBorder; + [CEReactions, ReflectURL] attribute USVString longDesc; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginHeight; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString marginWidth; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginHeight; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString marginWidth; }; partial interface HTMLImageElement { - [CEReactions] attribute DOMString name; - [CEReactions] attribute USVString lowsrc; - [CEReactions] attribute DOMString align; - [CEReactions] attribute unsigned long hspace; - [CEReactions] attribute unsigned long vspace; - [CEReactions] attribute USVString longDesc; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, ReflectURL] attribute USVString lowsrc; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute unsigned long hspace; + [CEReactions, Reflect] attribute unsigned long vspace; + [CEReactions, ReflectURL] attribute USVString longDesc; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString border; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString border; }; partial interface HTMLInputElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString useMap; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString useMap; }; partial interface HTMLLegendElement { - [CEReactions] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString align; }; partial interface HTMLLIElement { - [CEReactions] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString type; }; partial interface HTMLLinkElement { - [CEReactions] attribute DOMString charset; - [CEReactions] attribute DOMString rev; - [CEReactions] attribute DOMString target; + [CEReactions, Reflect] attribute DOMString charset; + [CEReactions, Reflect] attribute DOMString rev; + [CEReactions, Reflect] attribute DOMString target; }; partial interface HTMLMenuElement { - [CEReactions] attribute boolean compact; + [CEReactions, Reflect] attribute boolean compact; }; partial interface HTMLMetaElement { - [CEReactions] attribute DOMString scheme; + [CEReactions, Reflect] attribute DOMString scheme; }; partial interface HTMLObjectElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString archive; - [CEReactions] attribute DOMString code; - [CEReactions] attribute boolean declare; - [CEReactions] attribute unsigned long hspace; - [CEReactions] attribute DOMString standby; - [CEReactions] attribute unsigned long vspace; - [CEReactions] attribute DOMString codeBase; - [CEReactions] attribute DOMString codeType; - [CEReactions] attribute DOMString useMap; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString archive; + [CEReactions, Reflect] attribute DOMString code; + [CEReactions, Reflect] attribute boolean declare; + [CEReactions, Reflect] attribute unsigned long hspace; + [CEReactions, Reflect] attribute DOMString standby; + [CEReactions, Reflect] attribute unsigned long vspace; + [CEReactions, ReflectURL] attribute DOMString codeBase; + [CEReactions, Reflect] attribute DOMString codeType; + [CEReactions, Reflect] attribute DOMString useMap; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString border; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString border; }; partial interface HTMLOListElement { - [CEReactions] attribute boolean compact; + [CEReactions, Reflect] attribute boolean compact; }; partial interface HTMLParagraphElement { - [CEReactions] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString align; }; [Exposed=Window] interface HTMLParamElement : HTMLElement { [HTMLConstructor] constructor(); - [CEReactions] attribute DOMString name; - [CEReactions] attribute DOMString value; - [CEReactions] attribute DOMString type; - [CEReactions] attribute DOMString valueType; + [CEReactions, Reflect] attribute DOMString name; + [CEReactions, Reflect] attribute DOMString value; + [CEReactions, Reflect] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString valueType; }; partial interface HTMLPreElement { - [CEReactions] attribute long width; + [CEReactions, Reflect] attribute long width; }; partial interface HTMLStyleElement { - [CEReactions] attribute DOMString type; + [CEReactions, Reflect] attribute DOMString type; }; partial interface HTMLScriptElement { - [CEReactions] attribute DOMString charset; - [CEReactions] attribute DOMString event; - [CEReactions] attribute DOMString htmlFor; + [CEReactions, Reflect] attribute DOMString charset; + [CEReactions, Reflect] attribute DOMString event; + [CEReactions, Reflect="for"] attribute DOMString htmlFor; }; partial interface HTMLTableElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString border; - [CEReactions] attribute DOMString frame; - [CEReactions] attribute DOMString rules; - [CEReactions] attribute DOMString summary; - [CEReactions] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString border; + [CEReactions, Reflect] attribute DOMString frame; + [CEReactions, Reflect] attribute DOMString rules; + [CEReactions, Reflect] attribute DOMString summary; + [CEReactions, Reflect] attribute DOMString width; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellPadding; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString cellSpacing; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString cellPadding; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString cellSpacing; }; partial interface HTMLTableSectionElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString ch; - [CEReactions] attribute DOMString chOff; - [CEReactions] attribute DOMString vAlign; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect="char"] attribute DOMString ch; + [CEReactions, Reflect="charoff"] attribute DOMString chOff; + [CEReactions, Reflect] attribute DOMString vAlign; }; partial interface HTMLTableCellElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString axis; - [CEReactions] attribute DOMString height; - [CEReactions] attribute DOMString width; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect] attribute DOMString axis; + [CEReactions, Reflect] attribute DOMString height; + [CEReactions, Reflect] attribute DOMString width; - [CEReactions] attribute DOMString ch; - [CEReactions] attribute DOMString chOff; - [CEReactions] attribute boolean noWrap; - [CEReactions] attribute DOMString vAlign; + [CEReactions, Reflect="char"] attribute DOMString ch; + [CEReactions, Reflect="charoff"] attribute DOMString chOff; + [CEReactions, Reflect] attribute boolean noWrap; + [CEReactions, Reflect] attribute DOMString vAlign; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor; }; partial interface HTMLTableRowElement { - [CEReactions] attribute DOMString align; - [CEReactions] attribute DOMString ch; - [CEReactions] attribute DOMString chOff; - [CEReactions] attribute DOMString vAlign; + [CEReactions, Reflect] attribute DOMString align; + [CEReactions, Reflect="char"] attribute DOMString ch; + [CEReactions, Reflect="charoff"] attribute DOMString chOff; + [CEReactions, Reflect] attribute DOMString vAlign; - [CEReactions] attribute [LegacyNullToEmptyString] DOMString bgColor; + [CEReactions, Reflect] attribute [LegacyNullToEmptyString] DOMString bgColor; }; partial interface HTMLUListElement { - [CEReactions] attribute boolean compact; - [CEReactions] attribute DOMString type; + [CEReactions, Reflect] attribute boolean compact; + [CEReactions, Reflect] attribute DOMString type; }; partial interface Document { diff --git a/test/fixtures/wpt/interfaces/performance-timeline.idl b/test/fixtures/wpt/interfaces/performance-timeline.idl index 6ef84b6cbb8e60..f06ed9adb6c649 100644 --- a/test/fixtures/wpt/interfaces/performance-timeline.idl +++ b/test/fixtures/wpt/interfaces/performance-timeline.idl @@ -22,8 +22,8 @@ interface PerformanceEntry { }; callback PerformanceObserverCallback = undefined (PerformanceObserverEntryList entries, - PerformanceObserver observer, - optional PerformanceObserverCallbackOptions options = {}); + PerformanceObserver observer, + optional PerformanceObserverCallbackOptions options = {}); [Exposed=(Window,Worker)] interface PerformanceObserver { constructor(PerformanceObserverCallback callback); diff --git a/test/fixtures/wpt/interfaces/resource-timing.idl b/test/fixtures/wpt/interfaces/resource-timing.idl index 66f2841d744af3..ba8e3953b49705 100644 --- a/test/fixtures/wpt/interfaces/resource-timing.idl +++ b/test/fixtures/wpt/interfaces/resource-timing.idl @@ -22,12 +22,17 @@ interface PerformanceResourceTiming : PerformanceEntry { readonly attribute DOMHighResTimeStamp firstInterimResponseStart; readonly attribute DOMHighResTimeStamp responseStart; readonly attribute DOMHighResTimeStamp responseEnd; + readonly attribute DOMHighResTimeStamp workerRouterEvaluationStart; + readonly attribute DOMHighResTimeStamp workerCacheLookupStart; + readonly attribute DOMString workerMatchedRouterSource; + readonly attribute DOMString workerFinalRouterSource; readonly attribute unsigned long long transferSize; readonly attribute unsigned long long encodedBodySize; readonly attribute unsigned long long decodedBodySize; readonly attribute unsigned short responseStatus; readonly attribute RenderBlockingStatusType renderBlockingStatus; readonly attribute DOMString contentType; + readonly attribute DOMString contentEncoding; [Default] object toJSON(); }; @@ -36,8 +41,7 @@ enum RenderBlockingStatusType { "non-blocking" }; -partial interface Performance { - undefined clearResourceTimings (); +partial interface Performance { undefined clearResourceTimings (); undefined setResourceTimingBufferSize (unsigned long maxSize); attribute EventHandler onresourcetimingbufferfull; }; diff --git a/test/fixtures/wpt/interfaces/streams.idl b/test/fixtures/wpt/interfaces/streams.idl index ab9be033e43ba0..8abc8f5cfda9fe 100644 --- a/test/fixtures/wpt/interfaces/streams.idl +++ b/test/fixtures/wpt/interfaces/streams.idl @@ -17,7 +17,7 @@ interface ReadableStream { Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); sequence tee(); - async iterable(optional ReadableStreamIteratorOptions options = {}); + async_iterable(optional ReadableStreamIteratorOptions options = {}); }; typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; diff --git a/test/fixtures/wpt/interfaces/web-locks.idl b/test/fixtures/wpt/interfaces/web-locks.idl index 14bc3a22cc395f..00648cc3b1e5f4 100644 --- a/test/fixtures/wpt/interfaces/web-locks.idl +++ b/test/fixtures/wpt/interfaces/web-locks.idl @@ -1,3 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Web Locks API (https://w3c.github.io/web-locks/) + [SecureContext] interface mixin NavigatorLocks { readonly attribute LockManager locks; @@ -5,7 +10,7 @@ interface mixin NavigatorLocks { Navigator includes NavigatorLocks; WorkerNavigator includes NavigatorLocks; -[SecureContext, Exposed=(Window,Worker)] +[SecureContext, Exposed=(Window,Worker,SharedStorageWorklet)] interface LockManager { Promise request(DOMString name, LockGrantedCallback callback); @@ -38,7 +43,7 @@ dictionary LockInfo { DOMString clientId; }; -[SecureContext, Exposed=(Window,Worker)] +[SecureContext, Exposed=(Window,Worker,SharedStorageWorklet)] interface Lock { readonly attribute DOMString name; readonly attribute LockMode mode; diff --git a/test/fixtures/wpt/interfaces/webcrypto-modern-algos.idl b/test/fixtures/wpt/interfaces/webcrypto-modern-algos.idl new file mode 100644 index 00000000000000..aeb1d650b7d732 --- /dev/null +++ b/test/fixtures/wpt/interfaces/webcrypto-modern-algos.idl @@ -0,0 +1,118 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Modern Algorithms in the Web Cryptography API (https://wicg.github.io/webcrypto-modern-algos/) + +[SecureContext,Exposed=(Window,Worker)] +partial interface SubtleCrypto { + Promise encapsulateKey( + AlgorithmIdentifier encapsulationAlgorithm, + CryptoKey encapsulationKey, + AlgorithmIdentifier sharedKeyAlgorithm, + boolean extractable, + sequence keyUsages + ); + Promise encapsulateBits( + AlgorithmIdentifier encapsulationAlgorithm, + CryptoKey encapsulationKey + ); + + Promise decapsulateKey( + AlgorithmIdentifier decapsulationAlgorithm, + CryptoKey decapsulationKey, + BufferSource ciphertext, + AlgorithmIdentifier sharedKeyAlgorithm, + boolean extractable, + sequence keyUsages + ); + Promise decapsulateBits( + AlgorithmIdentifier decapsulationAlgorithm, + CryptoKey decapsulationKey, + BufferSource ciphertext + ); + + Promise getPublicKey( + CryptoKey key, + sequence keyUsages + ); + + static boolean supports(DOMString operation, + AlgorithmIdentifier algorithm, + optional unsigned long? length = null); + static boolean supports(DOMString operation, + AlgorithmIdentifier algorithm, + AlgorithmIdentifier additionalAlgorithm); +}; + +enum KeyFormat { "raw-public", "raw-private", "raw-seed", "raw-secret", "raw", "spki", "pkcs8", "jwk" }; + +enum KeyUsage { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits" }; + +dictionary EncapsulatedKey { + CryptoKey sharedKey; + ArrayBuffer ciphertext; +}; + +dictionary EncapsulatedBits { + ArrayBuffer sharedKey; + ArrayBuffer ciphertext; +}; + +partial dictionary JsonWebKey { + // The following fields are defined in draft-ietf-cose-dilithium-08 + DOMString pub; + DOMString priv; +}; + +dictionary ContextParams : Algorithm { + BufferSource context; +}; + +dictionary AeadParams : Algorithm { + required BufferSource iv; + BufferSource additionalData; + [EnforceRange] octet tagLength; +}; + +dictionary CShakeParams : Algorithm { + required [EnforceRange] unsigned long outputLength; + BufferSource functionName; + BufferSource customization; +}; + +dictionary TurboShakeParams : Algorithm { + required [EnforceRange] unsigned long outputLength; + [EnforceRange] octet domainSeparation; +}; + +dictionary KangarooTwelveParams : Algorithm { + required [EnforceRange] unsigned long outputLength; + BufferSource customization; +}; + +dictionary KmacKeyGenParams : Algorithm { + [EnforceRange] unsigned long length; +}; + +dictionary KmacImportParams : Algorithm { + [EnforceRange] unsigned long length; +}; + +dictionary KmacKeyAlgorithm : KeyAlgorithm { + required unsigned long length; +}; + +dictionary KmacParams : Algorithm { + required [EnforceRange] unsigned long outputLength; + BufferSource customization; +}; + +dictionary Argon2Params : Algorithm { + required BufferSource nonce; + required [EnforceRange] unsigned long parallelism; + required [EnforceRange] unsigned long memory; + required [EnforceRange] unsigned long passes; + [EnforceRange] octet version; + BufferSource secretValue; + BufferSource associatedData; +}; diff --git a/test/fixtures/wpt/interfaces/webcrypto-secure-curves.idl b/test/fixtures/wpt/interfaces/webcrypto-secure-curves.idl new file mode 100644 index 00000000000000..01bb290b747827 --- /dev/null +++ b/test/fixtures/wpt/interfaces/webcrypto-secure-curves.idl @@ -0,0 +1,8 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Secure Curves in the Web Cryptography API (https://wicg.github.io/webcrypto-secure-curves/) + +dictionary Ed448Params : Algorithm { + BufferSource context; +}; diff --git a/test/fixtures/wpt/interfaces/webcrypto.idl b/test/fixtures/wpt/interfaces/webcrypto.idl index ff7a89cd0d51be..ebb9cf5718b98d 100644 --- a/test/fixtures/wpt/interfaces/webcrypto.idl +++ b/test/fixtures/wpt/interfaces/webcrypto.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Web Cryptography API (https://w3c.github.io/webcrypto/) +// Source: Web Cryptography API Level 2 (https://w3c.github.io/webcrypto/) partial interface mixin WindowOrWorkerGlobalScope { [SameObject] readonly attribute Crypto crypto; @@ -28,8 +28,6 @@ dictionary KeyAlgorithm { enum KeyType { "public", "private", "secret" }; -enum KeyUsage { "encrypt", "decrypt", "sign", "verify", "deriveKey", "deriveBits", "wrapKey", "unwrapKey" }; - [SecureContext,Exposed=(Window,Worker),Serializable] interface CryptoKey { readonly attribute KeyType type; @@ -38,8 +36,6 @@ interface CryptoKey { readonly attribute object usages; }; -enum KeyFormat { "raw", "spki", "pkcs8", "jwk" }; - [SecureContext,Exposed=(Window,Worker)] interface SubtleCrypto { Promise encrypt( diff --git a/test/fixtures/wpt/interfaces/webidl.idl b/test/fixtures/wpt/interfaces/webidl.idl index f3db91096ac1be..651c1922115026 100644 --- a/test/fixtures/wpt/interfaces/webidl.idl +++ b/test/fixtures/wpt/interfaces/webidl.idl @@ -3,6 +3,19 @@ // (https://github.com/w3c/webref) // Source: Web IDL Standard (https://webidl.spec.whatwg.org/) +[Exposed=*, Serializable] +interface QuotaExceededError : DOMException { + constructor(optional DOMString message = "", optional QuotaExceededErrorOptions options = {}); + + readonly attribute double? quota; + readonly attribute double? requested; +}; + +dictionary QuotaExceededErrorOptions { + double quota; + double requested; +}; + typedef (Int8Array or Int16Array or Int32Array or Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or BigInt64Array or BigUint64Array or diff --git a/test/fixtures/wpt/resources/example.pdf b/test/fixtures/wpt/resources/example.pdf new file mode 100644 index 00000000000000..7bad251ba7e08e Binary files /dev/null and b/test/fixtures/wpt/resources/example.pdf differ diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js index 2eb710c1827cda..57cefedc22a182 100644 --- a/test/fixtures/wpt/resources/idlharness.js +++ b/test/fixtures/wpt/resources/idlharness.js @@ -1398,7 +1398,7 @@ IdlInterface.prototype.default_to_json_operation = function() { if (I.has_default_to_json_regular_operation()) { isDefault = true; for (const m of I.members) { - if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) { + if (!m.untested && m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) { map.set(m.name, m.idlType); } } diff --git a/test/fixtures/wpt/resources/testdriver-actions.js b/test/fixtures/wpt/resources/testdriver-actions.js index edb4759954d4c3..616a90b36b1b27 100644 --- a/test/fixtures/wpt/resources/testdriver-actions.js +++ b/test/fixtures/wpt/resources/testdriver-actions.js @@ -252,7 +252,7 @@ */ addTick: function(duration) { this.tickIdx += 1; - if (duration) { + if (duration !== undefined && duration !== null) { this.pause(duration); } return this; @@ -279,6 +279,10 @@ /** * Create a keyDown event for the current default key source * + * To send special keys, send the respective key's codepoint, + * as defined by `WebDriver + * `_. + * * @param {String} key - Key to press * @param {String?} sourceName - Named key source to use or null for the default key source * @returns {Actions} @@ -292,6 +296,10 @@ /** * Create a keyUp event for the current default key source * + * To send special keys, send the respective key's codepoint, + * as defined by `WebDriver + * `_. + * * @param {String} key - Key to release * @param {String?} sourceName - Named key source to use or null for the default key source * @returns {Actions} @@ -536,7 +544,7 @@ tick = actions.addTick().tickIdx; } let moveAction = {type: "pointerMove", x, y, origin}; - if (duration) { + if (duration !== undefined && duration !== null) { moveAction.duration = duration; } let actionProperties = setPointerProperties(moveAction, width, height, pressure, @@ -581,7 +589,7 @@ tick = actions.addTick().tickIdx; } this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin}); - if (duration) { + if (duration !== undefined && duration !== null) { this.actions.get(tick).duration = duration; } }, diff --git a/test/fixtures/wpt/resources/testdriver.js b/test/fixtures/wpt/resources/testdriver.js index 5b390dedeb72bb..4402fbd2235318 100644 --- a/test/fixtures/wpt/resources/testdriver.js +++ b/test/fixtures/wpt/resources/testdriver.js @@ -23,6 +23,14 @@ } } + function assertTestIsTentative(){ + const testPath = location.pathname; + const tentative = testPath.includes('.tentative.') || testPath.includes('/tentative/'); + if (!tentative) { + throw new Error("Method in testdriver.js intended for tentative tests used in non-tentative test"); + } + } + function getInViewCenterPoint(rect) { var left = Math.max(0, rect.left); var right = Math.min(window.innerWidth, rect.right); @@ -769,6 +777,78 @@ }, } }, + /** + * `speculation `_ module. + */ + speculation: { + /** + * `speculation.PrefetchStatusUpdated `_ + * event. + */ + prefetch_status_updated: { + /** + * @typedef {object} PrefetchStatusUpdated + * `speculation.PrefetchStatusUpdatedParameters `_ + * event. + */ + + /** + * Subscribes to the event. Events will be emitted only if + * there is a subscription for the event. This method does + * not add actual listeners. To listen to the event, use the + * `on` or `once` methods. The buffered events will be + * emitted before the command promise is resolved. + * + * @param {object} [params] Parameters for the subscription. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing + * contexts to subscribe to the event on. It should be + * either an array of Context objects, or null. If null, the + * event will be subscribed to globally. If omitted, the + * event will be subscribed to on the current browsing + * context. + * @returns {Promise<(function(): Promise)>} Callback + * for unsubscribing from the created subscription. + */ + subscribe: async function(params = {}) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.speculation + .prefetch_status_updated.subscribe(params); + }, + /** + * Adds an event listener for the event. + * + * @param {function(PrefetchStatusUpdated): void} callback The + * callback to be called when the event is emitted. The + * callback is called with the event object as a parameter. + * @returns {function(): void} A function that removes the + * added event listener when called. + */ + on: function(callback) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.speculation + .prefetch_status_updated.on(callback); + }, + /** + * Adds an event listener for the event that is only called + * once and removed afterward. + * + * @return {Promise} The promise which + * is resolved with the event object when the event is emitted. + */ + once: function() { + assertBidiIsEnabled(); + return new Promise(resolve => { + const remove_handler = + window.test_driver_internal.bidi.speculation + .prefetch_status_updated.on(event => { + resolve(event); + remove_handler(); + }); + }); + } + } + }, /** * `emulation `_ module. */ @@ -882,6 +962,61 @@ return window.test_driver_internal.bidi.emulation.set_screen_orientation_override( params); }, + /** + * Overrides the touch configuration for the specified browsing + * contexts. + * Matches the `emulation.setTouchOverride + * `_ + * WebDriver BiDi command. + * + * @example + * await test_driver.bidi.emulation.set_touch_override({ + * maxTouchPoints: 5 + * }); + * + * @param {object} params - Parameters for the command. + * @param {null|number} params.maxTouchPoints - The + * maximum number of simultaneous touch points to support. + * If null or omitted, the override will be removed. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing contexts + * to set the touch override on. It should be either an array of + * Context objects (window or browsing context id), or null. If + * null or omitted, the override will be set on the current + * browsing context. + * @returns {Promise} Resolves when the touch + * override is successfully set. + */ + set_touch_override: function (params) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.emulation.set_touch_override( + params); + }, + }, + /** + * `user_agent_client_hints `_ module. + */ + user_agent_client_hints: { + /** + * Overrides the user agent client hints configuration for the specified browsing + * contexts. Matches the `userAgentClientHints.setClientHintsOverride + * `_ + * WebDriver BiDi command. + * + * @param {object} params - Parameters for the command. + * @param {null|object} params.clientHints - The client hints to override. + * Matches the `userAgentClientHints.ClientHints` type. + * If null or omitted, the override will be removed. + * @param {null|Array.<(Context)>} [params.contexts] The + * optional contexts parameter specifies which browsing contexts + * to set the override on. + * @returns {Promise} Resolves when the override is successfully set. + */ + set_client_hints_override: function (params) { + assertBidiIsEnabled(); + return window.test_driver_internal.bidi.user_agent_client_hints.set_client_hints_override( + params); + } }, /** * `log `_ module. @@ -973,9 +1108,12 @@ * @param {PermissionState} params.state - a `PermissionState * `_ * value. - * @param {string} [params.origin] - an optional `origin` string to set the + * @param {string} [params.origin] - an optional top-level `origin` string to set the * permission for. If omitted, the permission is set for the * current window's origin. + * @param {string} [params.embeddedOrigin] - an optional embedded `origin` string to set the + * permission for. If omitted, the top-level `origin` is used as the + * embedded origin. * @returns {Promise} fulfilled after the permission is set, or rejected if setting * the permission fails. */ @@ -1191,6 +1329,34 @@ return role; }, + /** + * Get accessibility properties for a DOM element. + * + * @param {Element} element + * @returns {Promise} fulfilled after the accessibility properties are + * returned, or rejected in the cases the WebDriver + * command errors + */ + get_accessibility_properties_for_element: async function(element) { + assertTestIsTentative(); + let acc = await window.test_driver_internal.get_accessibility_properties_for_element(element); + return acc; + }, + + /** + * Get properties for an accessibility node. + * + * @param {String} accId + * @returns {Promise} fulfilled after the accessibility properties are + * returned, or rejected in the cases the WebDriver + * command errors + */ + get_accessibility_properties_for_accessibility_node: async function(accId) { + assertTestIsTentative(); + let acc = await window.test_driver_internal.get_accessibility_properties_for_accessibility_node(accId); + return acc; + }, + /** * Send keys to an element. * @@ -2169,6 +2335,69 @@ */ clear_display_features: function(context=null) { return window.test_driver_internal.clear_display_features(context); + }, + + /** + * Gets the current globally-applied privacy control status + * + * @returns {Promise} Fulfils with an object with boolean property `gpc` + * that encodes the current "do not sell or share" + * signal the browser is configured to convey. + */ + get_global_privacy_control: function() { + return window.test_driver_internal.get_global_privacy_control(); + }, + + /** + * Gets the current globally-applied privacy control status + * + * @param {bool} newValue - The a boolean that is true if the browers + * should convey a "do not sell or share" signal + * and false otherwise + * + * @returns {Promise} Fulfils with an object with boolean property `gpc` + * that encodes the new "do not sell or share" + * after applying the new value. + */ + set_global_privacy_control: function(newValue) { + return window.test_driver_internal.set_global_privacy_control(newValue); + }, + + /** + * Installs a WebExtension. + * + * Matches the `Install WebExtension + * `_ + * WebDriver command. + * + * @param {Object} params - Parameters for loading the extension. + * @param {String} params.type - A type such as "path", "archivePath", or "base64". + * + * @param {String} params.path - The path to the extension's resources if type "path" or "archivePath" is specified. + * + * @param {String} params.value - The base64 encoded value of the extension's resources if type "base64" is specified. + * + * @returns {Promise} Returns the extension identifier as defined in the spec. + * Rejected if the extension fails to load. + */ + install_web_extension: function(params) { + return window.test_driver_internal.install_web_extension(params); + }, + + /** + * Uninstalls a WebExtension. + * + * Matches the `Uninstall WebExtension + * `_ + * WebDriver command. + * + * @param {String} extension_id - The extension identifier. + * + * @returns {Promise} Fulfilled after the extension has been removed. + * Rejected in case the WebDriver command errors out. + */ + uninstall_web_extension: function(extension_id) { + return window.test_driver_internal.uninstall_web_extension(extension_id); } }; @@ -2252,6 +2481,16 @@ set_screen_orientation_override: function (params) { throw new Error( "bidi.emulation.set_screen_orientation_override is not implemented by testdriver-vendor.js"); + }, + set_touch_override: function (params) { + throw new Error( + "bidi.emulation.set_touch_override is not implemented by testdriver-vendor.js"); + } + }, + user_agent_client_hints: { + set_client_hints_override: function (params) { + throw new Error( + "bidi.user_agent_client_hints.set_client_hints_override is not implemented by testdriver-vendor.js"); } }, log: { @@ -2271,6 +2510,18 @@ throw new Error( "bidi.permissions.set_permission() is not implemented by testdriver-vendor.js"); } + }, + speculation: { + prefetch_status_updated: { + async subscribe() { + throw new Error( + 'bidi.speculation.prefetch_status_updated.subscribe is not implemented by testdriver-vendor.js'); + }, + on() { + throw new Error( + 'bidi.speculation.prefetch_status_updated.on is not implemented by testdriver-vendor.js'); + } + }, } }, @@ -2304,6 +2555,14 @@ throw new Error("get_computed_name is a testdriver.js function which cannot be run in this context."); }, + async get_accessibility_properties_for_element(element) { + throw new Error("get_accessibility_properties_for_element is a testdriver.js function which cannot be run in this context."); + }, + + async get_accessibility_properties_for_accessibility_node(accId) { + throw new Error("get_accessibility_properties_for_accessibility_node is a testdriver.js function which cannot be run in this context."); + }, + async send_keys(element, keys) { if (this.in_automation) { throw new Error("send_keys() is not implemented by testdriver-vendor.js"); @@ -2486,6 +2745,14 @@ async clear_display_features(context=null) { throw new Error("clear_display_features() is not implemented by testdriver-vendor.js"); + }, + + async set_global_privacy_control(newValue) { + throw new Error("set_global_privacy_control() is not implemented by testdriver-vendor.js"); + }, + + async get_global_privacy_control() { + throw new Error("get_global_privacy_control() is not implemented by testdriver-vendor.js"); } }; })(); diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js index f495b62458ba75..c7ce4f51e1db07 100644 --- a/test/fixtures/wpt/resources/testharness.js +++ b/test/fixtures/wpt/resources/testharness.js @@ -5144,7 +5144,7 @@ table#results.assertions > tbody > tr > td:last-child {\ width:35%;\ }\ \ -table#results > thead > > tr > th {\ +table#results > thead > tr > th {\ padding:0;\ padding-bottom:0.5em;\ border-bottom:medium solid black;\ diff --git a/test/fixtures/wpt/resources/web-extensions-helper.js b/test/fixtures/wpt/resources/web-extensions-helper.js new file mode 100644 index 00000000000000..57a40fe84dae0a --- /dev/null +++ b/test/fixtures/wpt/resources/web-extensions-helper.js @@ -0,0 +1,40 @@ +// testharness file with WebExtensions utilities + +/** + * Loads the WebExtension at the path specified and runs the tests defined in the extension's resources. + * Listens to messages sent from the user agent and converts the `browser.test` assertions + * into testharness.js assertions. + * + * @param {string} extensionPath - a path to the extension's resources. + */ + +setup({ explicit_done: true }) +globalThis.runTestsWithWebExtension = function(extensionPath) { + test_driver.install_web_extension({ + type: "path", + path: extensionPath + }) + .then((result) => { + let test; + browser.test.onTestStarted.addListener((data) => { + test = async_test(data.testName) + }) + + browser.test.onTestFinished.addListener((data) => { + test.step(() => { + let description = data.message ? `${data.assertionDescription}. ${data.message}` : data.assertionDescription + assert_true(data.result, description) + }) + + test.done() + + if (!data.result) { + test.set_status(test.FAIL) + } + + if (!data.remainingTests) { + test_driver.uninstall_web_extension(result.extension).then(() => { done() }) + } + }) + }) +} diff --git a/test/fixtures/wpt/resources/webidl2/lib/VERSION.md b/test/fixtures/wpt/resources/webidl2/lib/VERSION.md index 5a3726c6c00fe5..2614a8d194be62 100644 --- a/test/fixtures/wpt/resources/webidl2/lib/VERSION.md +++ b/test/fixtures/wpt/resources/webidl2/lib/VERSION.md @@ -1 +1 @@ -Currently using webidl2.js@6889aee6fc7d65915ab1267825248157dbc50486. +Currently using webidl2.js@e6d8ab852ec4e76596f6e308eb7f2efc8b613bfd. diff --git a/test/fixtures/wpt/resources/webidl2/lib/webidl2.js b/test/fixtures/wpt/resources/webidl2/lib/webidl2.js index 7161def899cf24..bae0b2047595d0 100644 --- a/test/fixtures/wpt/resources/webidl2/lib/webidl2.js +++ b/test/fixtures/wpt/resources/webidl2/lib/webidl2.js @@ -17,7 +17,7 @@ return /******/ (() => { // webpackBootstrap __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "parse": () => (/* binding */ parse) +/* harmony export */ parse: () => (/* binding */ parse) /* harmony export */ }); /* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(15); @@ -46,11 +46,22 @@ __webpack_require__.r(__webpack_exports__); +/** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ +/** @typedef {{ extMembers?: import("./productions/container.js").AllowedMember[]}} Extension */ +/** @typedef {Partial>} Extensions */ + +/** + * Parser options. + * @typedef {Object} ParserOptions + * @property {string} [sourceName] + * @property {boolean} [concrete] + * @property {Function[]} [productions] + * @property {Extensions} [extensions] + */ + /** * @param {Tokeniser} tokeniser - * @param {object} options - * @param {boolean} [options.concrete] - * @param {Function[]} [options.productions] + * @param {ParserOptions} options */ function parseByTokens(tokeniser, options) { const source = tokeniser.source; @@ -67,7 +78,9 @@ function parseByTokens(tokeniser, options) { const callback = consume("callback"); if (!callback) return; if (tokeniser.probe("interface")) { - return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback); + return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback, { + ...options?.extensions?.callbackInterface, + }); } return _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__.CallbackFunction.parse(tokeniser, callback); } @@ -75,20 +88,32 @@ function parseByTokens(tokeniser, options) { function interface_(opts) { const base = consume("interface"); if (!base) return; - const ret = - _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, opts) || - _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, opts) || - error("Interface has no proper body"); - return ret; + return ( + _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.mixin, + }) || + _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.interface, + }) || + error("Interface has no proper body") + ); } function partial() { const partial = consume("partial"); if (!partial) return; return ( - _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { partial }) || + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { + partial, + ...options?.extensions?.dictionary, + }) || interface_({ partial }) || - _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { partial }) || + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { + partial, + ...options?.extensions?.namespace, + }) || error("Partial doesn't apply to anything") ); } @@ -107,11 +132,11 @@ function parseByTokens(tokeniser, options) { callback() || interface_() || partial() || - _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser) || + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, options?.extensions?.dictionary) || _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__.Enum.parse(tokeniser) || _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__.Typedef.parse(tokeniser) || _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__.Includes.parse(tokeniser) || - _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser) + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, options?.extensions?.namespace) ); } @@ -134,6 +159,7 @@ function parseByTokens(tokeniser, options) { } return defs; } + const res = definitions(); if (tokeniser.position < source.length) error("Unrecognised tokens"); return res; @@ -141,11 +167,7 @@ function parseByTokens(tokeniser, options) { /** * @param {string} str - * @param {object} [options] - * @param {*} [options.sourceName] - * @param {boolean} [options.concrete] - * @param {Function[]} [options.productions] - * @return {import("./productions/base.js").Base[]} + * @param {ParserOptions} [options] */ function parse(str, options = {}) { const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str); @@ -163,11 +185,11 @@ function parse(str, options = {}) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Tokeniser": () => (/* binding */ Tokeniser), -/* harmony export */ "WebIDLParseError": () => (/* binding */ WebIDLParseError), -/* harmony export */ "argumentNameKeywords": () => (/* binding */ argumentNameKeywords), -/* harmony export */ "stringTypes": () => (/* binding */ stringTypes), -/* harmony export */ "typeNameKeywords": () => (/* binding */ typeNameKeywords) +/* harmony export */ Tokeniser: () => (/* binding */ Tokeniser), +/* harmony export */ WebIDLParseError: () => (/* binding */ WebIDLParseError), +/* harmony export */ argumentNameKeywords: () => (/* binding */ argumentNameKeywords), +/* harmony export */ stringTypes: () => (/* binding */ stringTypes), +/* harmony export */ typeNameKeywords: () => (/* binding */ typeNameKeywords) /* harmony export */ }); /* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); /* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -191,6 +213,7 @@ const tokenRe = { const typeNameKeywords = [ "ArrayBuffer", + "SharedArrayBuffer", "DataView", "Int8Array", "Int16Array", @@ -201,6 +224,7 @@ const typeNameKeywords = [ "Uint8ClampedArray", "BigInt64Array", "BigUint64Array", + "Float16Array", "Float32Array", "Float64Array", "any", @@ -243,6 +267,8 @@ const nonRegexTerminals = [ "NaN", "ObservableArray", "Promise", + "async_iterable", + "async_sequence", "bigint", "boolean", "byte", @@ -327,10 +353,10 @@ function tokenise(str) { if (result !== -1) { if (reserved.includes(token.value)) { const message = `${(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)( - token.value + token.value, )} is a reserved identifier and must not be used.`; throw new WebIDLParseError( - (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message) + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message), ); } else if (nonRegexTerminals.includes(token.value)) { token.type = "inline"; @@ -414,7 +440,7 @@ class Tokeniser { */ error(message) { throw new WebIDLParseError( - (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message) + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message), ); } @@ -522,8 +548,8 @@ class WebIDLParseError extends Error { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "syntaxError": () => (/* binding */ syntaxError), -/* harmony export */ "validationError": () => (/* binding */ validationError) +/* harmony export */ syntaxError: () => (/* binding */ syntaxError), +/* harmony export */ validationError: () => (/* binding */ validationError) /* harmony export */ }); /** * @param {string} text @@ -572,7 +598,7 @@ function error( current, message, kind, - { level = "error", autofix, ruleName } = {} + { level = "error", autofix, ruleName } = {}, ) { /** * @param {number} count @@ -606,11 +632,11 @@ function error( source[position].type !== "eof" ? source[position].line : source.length > 1 - ? source[position - 1].line - : 1; + ? source[position - 1].line + : 1; const precedingLastLine = lastLine( - tokensToText(sliceTokens(-maxTokens), { precedes: true }) + tokensToText(sliceTokens(-maxTokens), { precedes: true }), ); const subsequentTokens = sliceTokens(maxTokens); @@ -625,7 +651,7 @@ function error( const grammaticalContext = current && current.name ? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText( - current + current, )}\`` : ""; const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`; @@ -659,7 +685,7 @@ function validationError( current, ruleName, message, - options = {} + options = {}, ) { options.ruleName = ruleName; return error( @@ -668,7 +694,7 @@ function validationError( current, message, "Validation", - options + options, ); } @@ -679,21 +705,21 @@ function validationError( __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "argument_list": () => (/* binding */ argument_list), -/* harmony export */ "autoParenter": () => (/* binding */ autoParenter), -/* harmony export */ "autofixAddExposedWindow": () => (/* binding */ autofixAddExposedWindow), -/* harmony export */ "const_data": () => (/* binding */ const_data), -/* harmony export */ "const_value": () => (/* binding */ const_value), -/* harmony export */ "findLastIndex": () => (/* binding */ findLastIndex), -/* harmony export */ "getFirstToken": () => (/* binding */ getFirstToken), -/* harmony export */ "getLastIndentation": () => (/* binding */ getLastIndentation), -/* harmony export */ "getMemberIndentation": () => (/* binding */ getMemberIndentation), -/* harmony export */ "list": () => (/* binding */ list), -/* harmony export */ "primitive_type": () => (/* binding */ primitive_type), -/* harmony export */ "return_type": () => (/* binding */ return_type), -/* harmony export */ "stringifier": () => (/* binding */ stringifier), -/* harmony export */ "type_with_extended_attributes": () => (/* binding */ type_with_extended_attributes), -/* harmony export */ "unescape": () => (/* binding */ unescape) +/* harmony export */ argument_list: () => (/* binding */ argument_list), +/* harmony export */ autoParenter: () => (/* binding */ autoParenter), +/* harmony export */ autofixAddExposedWindow: () => (/* binding */ autofixAddExposedWindow), +/* harmony export */ const_data: () => (/* binding */ const_data), +/* harmony export */ const_value: () => (/* binding */ const_value), +/* harmony export */ findLastIndex: () => (/* binding */ findLastIndex), +/* harmony export */ getFirstToken: () => (/* binding */ getFirstToken), +/* harmony export */ getLastIndentation: () => (/* binding */ getLastIndentation), +/* harmony export */ getMemberIndentation: () => (/* binding */ getMemberIndentation), +/* harmony export */ list: () => (/* binding */ list), +/* harmony export */ primitive_type: () => (/* binding */ primitive_type), +/* harmony export */ return_type: () => (/* binding */ return_type), +/* harmony export */ stringifier: () => (/* binding */ stringifier), +/* harmony export */ type_with_extended_attributes: () => (/* binding */ type_with_extended_attributes), +/* harmony export */ unescape: () => (/* binding */ unescape) /* harmony export */ }); /* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5); /* harmony import */ var _argument_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); @@ -816,7 +842,7 @@ function primitive_type(tokeniser) { "boolean", "byte", "octet", - "undefined" + "undefined", ); if (base) { return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { base } }); @@ -917,7 +943,7 @@ function autofixAddExposedWindow(def) { def.extAttrs.unshift(exposed); } else { autoParenter(def).extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse( - new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]") + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]"), ); const trivia = def.tokens.base.trivia; def.extAttrs.tokens.open.trivia = trivia; @@ -1010,7 +1036,7 @@ function autoParenter(data, parent) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Type": () => (/* binding */ Type) +/* harmony export */ Type: () => (/* binding */ Type) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -1034,14 +1060,15 @@ function generic_type(tokeniser, typeName) { "FrozenArray", "ObservableArray", "Promise", + "async_sequence", "sequence", - "record" + "record", ); if (!base) { return; } const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( - new Type({ source: tokeniser.source, tokens: { base } }) + new Type({ source: tokeniser.source, tokens: { base } }), ); ret.tokens.open = tokeniser.consume("<") || @@ -1056,6 +1083,7 @@ function generic_type(tokeniser, typeName) { ret.subtype.push(subtype); break; } + case "async_sequence": case "sequence": case "FrozenArray": case "ObservableArray": { @@ -1143,7 +1171,7 @@ function union_type(tokeniser, type) { ret.type = type || null; while (true) { const typ = - (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) || + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, type) || tokeniser.error("No type after open parenthesis or 'or' in union type"); if (typ.idlType === "any") tokeniser.error("Type `any` cannot be included in a union type"); @@ -1157,7 +1185,7 @@ function union_type(tokeniser, type) { } if (ret.idlType.length < 2) { tokeniser.error( - "At least two types are expected in a union type but found less" + "At least two types are expected in a union type but found less", ); } tokens.close = @@ -1208,6 +1236,26 @@ class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { *validate(defs) { yield* this.extAttrs.validate(defs); + if (this.idlType === "BufferSource") { + // XXX: For now this is a hack. Consider moving parents' extAttrs into types as the spec says: + // https://webidl.spec.whatwg.org/#idl-annotated-types + for (const extAttrs of [this.extAttrs, this.parent?.extAttrs]) { + for (const extAttr of extAttrs) { + if (extAttr.name !== "AllowShared") { + continue; + } + const message = `\`[AllowShared] BufferSource\` is now replaced with AllowSharedBufferSource.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + this.tokens.base, + this, + "migrate-allowshared", + message, + { autofix: replaceAllowShared(this, extAttr, extAttrs) }, + ); + } + } + } + if (this.idlType === "void") { const message = `\`void\` is now replaced by \`undefined\`. Refer to the \ [relevant GitHub issue](https://github.com/whatwg/webidl/issues/60) \ @@ -1225,8 +1273,8 @@ for more information.`; const target = this.union ? this : typedef && typedef.type === "typedef" - ? typedef.idlType - : undefined; + ? typedef.idlType + : undefined; if (target && this.nullable) { // do not allow any dictionary const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__.idlTypeIncludesDictionary)(target, defs) || {}; @@ -1237,7 +1285,7 @@ for more information.`; targetToken, this, "no-nullable-union-dict", - message + message, ); } } else { @@ -1274,7 +1322,7 @@ for more information.`; this.idlType ), context: this, - } + }, ); return w.ts.wrap([w.ts.trivia(firstToken.trivia), ref]); }; @@ -1287,6 +1335,23 @@ for more information.`; } } +/** + * @param {Type} type + * @param {import("./extended-attributes.js").SimpleExtendedAttribute} extAttr + * @param {ExtendedAttributes} extAttrs + */ +function replaceAllowShared(type, extAttr, extAttrs) { + return () => { + const index = extAttrs.indexOf(extAttr); + extAttrs.splice(index, 1); + if (!extAttrs.length && type.tokens.base.trivia.match(/^\s$/)) { + type.tokens.base.trivia = ""; // (let's not remove comments) + } + + type.tokens.base.value = "AllowSharedBufferSource"; + }; +} + /** * @param {Type} type */ @@ -1303,7 +1368,7 @@ function replaceVoid(type) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Base": () => (/* binding */ Base) +/* harmony export */ Base: () => (/* binding */ Base) /* harmony export */ }); class Base { /** @@ -1344,14 +1409,17 @@ class Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "dictionaryIncludesRequiredField": () => (/* binding */ dictionaryIncludesRequiredField), -/* harmony export */ "idlTypeIncludesDictionary": () => (/* binding */ idlTypeIncludesDictionary) +/* harmony export */ dictionaryIncludesRequiredField: () => (/* binding */ dictionaryIncludesRequiredField), +/* harmony export */ idlTypeIncludesDictionary: () => (/* binding */ idlTypeIncludesDictionary), +/* harmony export */ idlTypeIncludesEnforceRange: () => (/* binding */ idlTypeIncludesEnforceRange) /* harmony export */ }); /** + * @typedef {import("../validator.js").Definitions} Definitions * @typedef {import("../productions/dictionary.js").Dictionary} Dictionary + * @typedef {import("../../lib/productions/type").Type} Type * - * @param {*} idlType - * @param {import("../validator.js").Definitions} defs + * @param {Type} idlType + * @param {Definitions} defs * @param {object} [options] * @param {boolean} [options.useNullableInner] use when the input idlType is nullable and you want to use its inner type * @return {{ reference: *, dictionary: Dictionary }} the type reference that ultimately includes dictionary. @@ -1359,7 +1427,7 @@ __webpack_require__.r(__webpack_exports__); function idlTypeIncludesDictionary( idlType, defs, - { useNullableInner } = {} + { useNullableInner } = {}, ) { if (!idlType.union) { const def = defs.unique.get(idlType.idlType); @@ -1405,8 +1473,8 @@ function idlTypeIncludesDictionary( } /** - * @param {*} dict dictionary type - * @param {import("../validator.js").Definitions} defs + * @param {Dictionary} dict dictionary type + * @param {Definitions} defs * @return {boolean} */ function dictionaryIncludesRequiredField(dict, defs) { @@ -1430,6 +1498,34 @@ function dictionaryIncludesRequiredField(dict, defs) { return result; } +/** + * For now this only checks the most frequent cases: + * 1. direct inclusion of [EnforceRange] + * 2. typedef of that + * + * More complex cases with dictionaries and records are not covered yet. + * + * @param {Type} idlType + * @param {Definitions} defs + */ +function idlTypeIncludesEnforceRange(idlType, defs) { + if (idlType.union) { + // TODO: This should ideally be checked too + return false; + } + + if (idlType.extAttrs.some((e) => e.name === "EnforceRange")) { + return true; + } + + const def = defs.unique.get(idlType.idlType); + if (def?.type !== "typedef") { + return false; + } + + return def.idlType.extAttrs.some((e) => e.name === "EnforceRange"); +} + /***/ }), /* 8 */ @@ -1437,9 +1533,9 @@ function dictionaryIncludesRequiredField(dict, defs) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "ExtendedAttributeParameters": () => (/* binding */ ExtendedAttributeParameters), -/* harmony export */ "ExtendedAttributes": () => (/* binding */ ExtendedAttributes), -/* harmony export */ "SimpleExtendedAttribute": () => (/* binding */ SimpleExtendedAttribute) +/* harmony export */ ExtendedAttributeParameters: () => (/* binding */ ExtendedAttributeParameters), +/* harmony export */ ExtendedAttributes: () => (/* binding */ ExtendedAttributes), +/* harmony export */ SimpleExtendedAttribute: () => (/* binding */ SimpleExtendedAttribute) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _array_base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9); @@ -1494,7 +1590,7 @@ function extAttrListItems(tokeniser) { } } tokeniser.error( - `Expected identifiers, strings, decimals, or integers but none found` + `Expected identifiers, strings, decimals, or integers but none found`, ); } @@ -1505,7 +1601,7 @@ class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__. static parse(tokeniser) { const tokens = { assign: tokeniser.consume("=") }; const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( - new ExtendedAttributeParameters({ source: tokeniser.source, tokens }) + new ExtendedAttributeParameters({ source: tokeniser.source, tokens }), ); ret.list = []; if (tokens.assign) { @@ -1603,8 +1699,8 @@ class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base const value = this.params.rhsIsList ? list : this.params.tokens.secondaryName - ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value) - : null; + ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value) + : null; return { type, value }; } get arguments() { @@ -1627,7 +1723,7 @@ information.`; this, "no-nointerfaceobject", message, - { level: "warning" } + { level: "warning" }, ); } else if (renamedLegacies.has(name)) { const message = `\`[${name}]\` extended attribute is a legacy feature \ @@ -1652,7 +1748,7 @@ information.`; w.ts.wrap([ w.ts.extendedAttributeReference(this.name), this.params.write(w), - ]) + ]), ), w.token(this.tokens.separator), ]); @@ -1687,12 +1783,12 @@ class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.Arr ...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, { parser: SimpleExtendedAttribute.parse, listName: "extended attribute", - }) + }), ); tokens.close = tokeniser.consume("]") || tokeniser.error( - "Expected a closing token for the extended attribute list" + "Expected a closing token for the extended attribute list", ); if (!ret.length) { tokeniser.unconsume(tokens.close.index); @@ -1700,7 +1796,7 @@ class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.Arr } if (tokeniser.probe("[")) { tokeniser.error( - "Illegal double extended attribute lists, consider merging them" + "Illegal double extended attribute lists, consider merging them", ); } return ret; @@ -1730,7 +1826,7 @@ class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.Arr __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "ArrayBase": () => (/* binding */ ArrayBase) +/* harmony export */ ArrayBase: () => (/* binding */ ArrayBase) /* harmony export */ }); class ArrayBase extends Array { constructor({ source, tokens }) { @@ -1750,8 +1846,8 @@ class ArrayBase extends Array { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Eof": () => (/* binding */ Eof), -/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken) +/* harmony export */ Eof: () => (/* binding */ Eof), +/* harmony export */ WrappedToken: () => (/* binding */ WrappedToken) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -1775,6 +1871,10 @@ class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { }; } + get type() { + return this.tokens.value.type; + } + get value() { return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value); } @@ -1811,7 +1911,7 @@ class Eof extends WrappedToken { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Argument": () => (/* binding */ Argument) +/* harmony export */ Argument: () => (/* binding */ Argument) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12); @@ -1837,7 +1937,7 @@ class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { /** @type {Base["tokens"]} */ const tokens = {}; const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( - new Argument({ source: tokeniser.source, tokens }) + new Argument({ source: tokeniser.source, tokens }), ); ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); tokens.optional = tokeniser.consume("optional"); @@ -1887,7 +1987,7 @@ class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { this.tokens.name, this, "no-nullable-dict-arg", - message + message, ); } else if (!this.optional) { if ( @@ -1903,7 +2003,7 @@ class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { message, { autofix: autofixDictionaryArgumentOptionality(this), - } + }, ); } } else if (!this.default) { @@ -1915,7 +2015,7 @@ class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { message, { autofix: autofixOptionalDictionaryDefaultValue(this), - } + }, ); } } @@ -1977,7 +2077,7 @@ function autofixOptionalDictionaryDefaultValue(arg) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Default": () => (/* binding */ Default) +/* harmony export */ Default: () => (/* binding */ Default) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -2049,7 +2149,7 @@ class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Operation": () => (/* binding */ Operation) +/* harmony export */ Operation: () => (/* binding */ Operation) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -2060,17 +2160,15 @@ __webpack_require__.r(__webpack_exports__); class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { /** - * @typedef {import("../tokeniser.js").Token} Token - * * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] - * @param {Token} [options.special] - * @param {Token} [options.regular] + * @param {import("../tokeniser.js").Token} [options.special] + * @param {import("../tokeniser.js").Token} [options.regular] */ static parse(tokeniser, { special, regular } = {}) { const tokens = { special }; const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( - new Operation({ source: tokeniser.source, tokens }) + new Operation({ source: tokeniser.source, tokens }), ); if (special && special.value === "stringifier") { tokens.termination = tokeniser.consume(";"); @@ -2121,6 +2219,15 @@ class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)(this.tokens.open, this, "incomplete-op", message); } if (this.idlType) { + if (this.idlType.generic === "async_sequence") { + const message = `async_sequence types cannot be returned by an operation.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)( + this.idlType.tokens.base, + this, + "async-sequence-idl-to-js", + message, + ); + } yield* this.idlType.validate(defs); } for (const argument of this.arguments) { @@ -2149,7 +2256,7 @@ class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { ...body, w.token(this.tokens.termination), ]), - { data: this, parent } + { data: this, parent }, ); } } @@ -2161,7 +2268,7 @@ class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Attribute": () => (/* binding */ Attribute) +/* harmony export */ Attribute: () => (/* binding */ Attribute) /* harmony export */ }); /* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); /* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7); @@ -2182,12 +2289,12 @@ class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { */ static parse( tokeniser, - { special, noInherit = false, readonly = false } = {} + { special, noInherit = false, readonly = false } = {}, ) { const start_position = tokeniser.position; const tokens = { special }; const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( - new Attribute({ source: tokeniser.source, tokens }) + new Attribute({ source: tokeniser.source, tokens }), ); if (!special && !noInherit) { tokens.special = tokeniser.consume("inherit"); @@ -2237,32 +2344,34 @@ class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { yield* this.extAttrs.validate(defs); yield* this.idlType.validate(defs); - switch (this.idlType.generic) { - case "sequence": - case "record": { - const message = `Attributes cannot accept ${this.idlType.generic} types.`; - yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( - this.tokens.name, - this, - "attr-invalid-type", - message - ); - break; + if ( + ["async_sequence", "sequence", "record"].includes(this.idlType.generic) + ) { + const message = `Attributes cannot accept ${this.idlType.generic} types.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + this.tokens.name, + this, + "attr-invalid-type", + message, + ); + } + + { + const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {}; + if (reference) { + const targetToken = (this.idlType.union ? reference : this.idlType) + .tokens.base; + const message = "Attributes cannot accept dictionary types."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(targetToken, this, "attr-invalid-type", message); } - default: { - const { reference } = - (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {}; - if (reference) { - const targetToken = (this.idlType.union ? reference : this.idlType) - .tokens.base; - const message = "Attributes cannot accept dictionary types."; - yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( - targetToken, - this, - "attr-invalid-type", - message - ); - } + } + + if (this.readonly) { + if ((0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesEnforceRange)(this.idlType, defs)) { + const targetToken = this.idlType.tokens.base; + const message = + "Readonly attributes cannot accept [EnforceRange] extended attribute."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(targetToken, this, "attr-invalid-type", message); } } } @@ -2280,7 +2389,7 @@ class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { w.name_token(this.tokens.name, { data: this, parent }), w.token(this.tokens.termination), ]), - { data: this, parent } + { data: this, parent }, ); } } @@ -2292,8 +2401,8 @@ class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Enum": () => (/* binding */ Enum), -/* harmony export */ "EnumValue": () => (/* binding */ EnumValue) +/* harmony export */ Enum: () => (/* binding */ Enum), +/* harmony export */ EnumValue: () => (/* binding */ EnumValue) /* harmony export */ }); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); /* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10); @@ -2327,7 +2436,7 @@ class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken { w.ts.trivia(this.tokens.value.trivia), w.ts.definition( w.ts.wrap(['"', w.ts.name(this.value, { data: this, parent }), '"']), - { data: this, parent } + { data: this, parent }, ), w.token(this.tokens.separator), ]); @@ -2388,7 +2497,7 @@ class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { w.token(this.tokens.close), w.token(this.tokens.termination), ]), - { data: this } + { data: this }, ); } } @@ -2400,7 +2509,7 @@ class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Includes": () => (/* binding */ Includes) +/* harmony export */ Includes: () => (/* binding */ Includes) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -2451,7 +2560,7 @@ class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.reference_token(this.tokens.mixin, this), w.token(this.tokens.termination), ]), - { data: this } + { data: this }, ); } } @@ -2463,7 +2572,7 @@ class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Typedef": () => (/* binding */ Typedef) +/* harmony export */ Typedef: () => (/* binding */ Typedef) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -2516,7 +2625,7 @@ class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.name_token(this.tokens.name, { data: this }), w.token(this.tokens.termination), ]), - { data: this } + { data: this }, ); } } @@ -2528,10 +2637,12 @@ class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "CallbackFunction": () => (/* binding */ CallbackFunction) +/* harmony export */ CallbackFunction: () => (/* binding */ CallbackFunction) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); + @@ -2542,7 +2653,7 @@ class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { static parse(tokeniser, base) { const tokens = { base }; const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( - new CallbackFunction({ source: tokeniser.source, tokens }) + new CallbackFunction({ source: tokeniser.source, tokens }), ); tokens.name = tokeniser.consumeKind("identifier") || @@ -2573,6 +2684,18 @@ class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { *validate(defs) { yield* this.extAttrs.validate(defs); + for (const arg of this.arguments) { + yield* arg.validate(defs); + if (arg.idlType.generic === "async_sequence") { + const message = `async_sequence types cannot be returned as a callback argument.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)( + arg.tokens.name, + arg, + "async-sequence-idl-to-js", + message, + ); + } + } yield* this.idlType.validate(defs); } @@ -2590,7 +2713,7 @@ class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.close), w.token(this.tokens.termination), ]), - { data: this } + { data: this }, ); } } @@ -2602,7 +2725,7 @@ class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Interface": () => (/* binding */ Interface) +/* harmony export */ Interface: () => (/* binding */ Interface) /* harmony export */ }); /* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); /* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); @@ -2643,8 +2766,12 @@ function static_member(tokeniser) { class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {import("../tokeniser.js").Token} base + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token|null} [options.partial] */ - static parse(tokeniser, base, { partial = null } = {}) { + static parse(tokeniser, base, { extMembers = [], partial = null } = {}) { const tokens = { partial, base }; return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( tokeniser, @@ -2652,6 +2779,7 @@ class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { { inheritable: !partial, allowedMembers: [ + ...extMembers, [_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse], [_constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse], [static_member], @@ -2660,7 +2788,7 @@ class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse], [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse], ], - } + }, ); } @@ -2686,11 +2814,11 @@ for more information.`; message, { autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autofixAddExposedWindow)(this), - } + }, ); } const oldConstructors = this.extAttrs.filter( - (extAttr) => extAttr.name === "Constructor" + (extAttr) => extAttr.name === "Constructor", ); for (const constructor of oldConstructors) { const message = `Constructors should now be represented as a \`constructor()\` operation on the interface \ @@ -2704,14 +2832,14 @@ for more information.`; message, { autofix: autofixConstructor(this, constructor), - } + }, ); } const isGlobal = this.extAttrs.some((extAttr) => extAttr.name === "Global"); if (isGlobal) { const factoryFunctions = this.extAttrs.filter( - (extAttr) => extAttr.name === "LegacyFactoryFunction" + (extAttr) => extAttr.name === "LegacyFactoryFunction", ); for (const named of factoryFunctions) { const message = `Interfaces marked as \`[Global]\` cannot have factory functions.`; @@ -2719,12 +2847,12 @@ for more information.`; named.tokens.name, this, "no-constructible-global", - message + message, ); } const constructors = this.members.filter( - (member) => member.type === "constructor" + (member) => member.type === "constructor", ); for (const named of constructors) { const message = `Interfaces marked as \`[Global]\` cannot have constructors.`; @@ -2732,7 +2860,7 @@ for more information.`; named.tokens.base, this, "no-constructible-global", - message + message, ); } } @@ -2748,13 +2876,13 @@ function autofixConstructor(interfaceDef, constructorExtAttr) { interfaceDef = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(interfaceDef); return () => { const indentation = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)( - interfaceDef.extAttrs.tokens.open.trivia + interfaceDef.extAttrs.tokens.open.trivia, ); const memberIndent = interfaceDef.members.length ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)((0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getFirstToken)(interfaceDef.members[0]).trivia) : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getMemberIndentation)(indentation); const constructorOp = _constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse( - new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`) + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`), ); constructorOp.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__.ExtendedAttributes({ source: interfaceDef.source, @@ -2764,7 +2892,7 @@ function autofixConstructor(interfaceDef, constructorExtAttr) { const existingIndex = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.findLastIndex)( interfaceDef.members, - (m) => m.type === "constructor" + (m) => m.type === "constructor", ); interfaceDef.members.splice(existingIndex + 1, 0, constructorOp); @@ -2793,7 +2921,7 @@ function autofixConstructor(interfaceDef, constructorExtAttr) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Container": () => (/* binding */ Container) +/* harmony export */ Container: () => (/* binding */ Container) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8); @@ -2816,6 +2944,19 @@ function inheritance(tokeniser) { return { colon, inheritance }; } +/** + * Parser callback. + * @callback ParserCallback + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {...*} args + */ + +/** + * A parser callback and optional option object. + * @typedef AllowedMember + * @type {[ParserCallback, object?]} + */ + class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser @@ -2889,7 +3030,7 @@ class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.colon), w.ts.trivia(this.tokens.inheritance.trivia), w.ts.inheritance( - w.reference(this.tokens.inheritance.value, { context: this }) + w.reference(this.tokens.inheritance.value, { context: this }), ), ]); }; @@ -2908,7 +3049,7 @@ class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.close), w.token(this.tokens.termination), ]), - { data: this } + { data: this }, ); } } @@ -2920,7 +3061,7 @@ class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Constant": () => (/* binding */ Constant) +/* harmony export */ Constant: () => (/* binding */ Constant) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); @@ -2989,7 +3130,7 @@ class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.value), w.token(this.tokens.termination), ]), - { data: this, parent } + { data: this, parent }, ); } } @@ -3001,21 +3142,23 @@ class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "IterableLike": () => (/* binding */ IterableLike) +/* harmony export */ IterableLike: () => (/* binding */ IterableLike) /* harmony export */ }); -/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); -/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + -class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { +class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_1__.Base { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser */ static parse(tokeniser) { const start_position = tokeniser.position; - const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( - new IterableLike({ source: tokeniser.source, tokens: {} }) + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)( + new IterableLike({ source: tokeniser.source, tokens: {} }), ); const { tokens } = ret; tokens.readonly = tokeniser.consume("readonly"); @@ -3025,8 +3168,8 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { tokens.base = tokens.readonly ? tokeniser.consume("maplike", "setlike") : tokens.async - ? tokeniser.consume("iterable") - : tokeniser.consume("iterable", "maplike", "setlike"); + ? tokeniser.consume("iterable") + : tokeniser.consume("iterable", "async_iterable", "maplike", "setlike"); if (!tokens.base) { tokeniser.unconsume(start_position); return; @@ -3034,14 +3177,16 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { const { type } = ret; const secondTypeRequired = type === "maplike"; - const secondTypeAllowed = secondTypeRequired || type === "iterable"; - const argumentAllowed = ret.async && type === "iterable"; + const secondTypeAllowed = + secondTypeRequired || type === "iterable" || type === "async_iterable"; + const argumentAllowed = + type === "async_iterable" || (ret.async && type === "iterable"); tokens.open = tokeniser.consume("<") || tokeniser.error(`Missing less-than sign \`<\` in ${type} declaration`); const first = - (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) || + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.type_with_extended_attributes)(tokeniser) || tokeniser.error(`Missing a type argument in ${type} declaration`); ret.idlType = [first]; ret.arguments = []; @@ -3049,7 +3194,7 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { if (secondTypeAllowed) { first.tokens.separator = tokeniser.consume(","); if (first.tokens.separator) { - ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser)); + ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.type_with_extended_attributes)(tokeniser)); } else if (secondTypeRequired) { tokeniser.error(`Missing second type argument in ${type} declaration`); } @@ -3062,7 +3207,7 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { if (tokeniser.probe("(")) { if (argumentAllowed) { tokens.argsOpen = tokeniser.consume("("); - ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser)); + ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.argument_list)(tokeniser)); tokens.argsClose = tokeniser.consume(")") || tokeniser.error("Unterminated async iterable argument list"); @@ -3089,6 +3234,18 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { } *validate(defs) { + if (this.async && this.type === "iterable") { + const message = "`async iterable` is now changed to `async_iterable`."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + this.tokens.async, + this, + "obsolete-async-iterable-syntax", + message, + { + autofix: autofixAsyncIterableSyntax(this), + }, + ); + } for (const type of this.idlType) { yield* type.validate(defs); } @@ -3113,11 +3270,26 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.argsClose), w.token(this.tokens.termination), ]), - { data: this, parent: this.parent } + { data: this, parent: this.parent }, ); } } +/** + * @param {IterableLike} iterableLike + */ +function autofixAsyncIterableSyntax(iterableLike) { + return () => { + const async = iterableLike.tokens.async; + iterableLike.tokens.base = { + ...async, + type: "async_iterable", + value: "async_iterable", + }; + delete iterableLike.tokens.async; + }; +} + /***/ }), /* 23 */ @@ -3125,7 +3297,7 @@ class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "checkInterfaceMemberDuplication": () => (/* binding */ checkInterfaceMemberDuplication) +/* harmony export */ checkInterfaceMemberDuplication: () => (/* binding */ checkInterfaceMemberDuplication) /* harmony export */ }); /* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); @@ -3164,7 +3336,7 @@ function* checkInterfaceMemberDuplication(defs, i) { addition.tokens.name, ext, "no-cross-overload", - message + message, ); } } @@ -3185,10 +3357,10 @@ function* checkInterfaceMemberDuplication(defs, i) { const ops = getOperations(i); return { statics: new Set( - ops.filter((op) => op.special === "static").map((op) => op.name) + ops.filter((op) => op.special === "static").map((op) => op.name), ), nonstatics: new Set( - ops.filter((op) => op.special !== "static").map((op) => op.name) + ops.filter((op) => op.special !== "static").map((op) => op.name), ), }; } @@ -3201,7 +3373,7 @@ function* checkInterfaceMemberDuplication(defs, i) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Constructor": () => (/* binding */ Constructor) +/* harmony export */ Constructor: () => (/* binding */ Constructor) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -3255,7 +3427,7 @@ class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { w.token(this.tokens.close), w.token(this.tokens.termination), ]), - { data: this, parent } + { data: this, parent }, ); } } @@ -3267,7 +3439,7 @@ class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Mixin": () => (/* binding */ Mixin) +/* harmony export */ Mixin: () => (/* binding */ Mixin) /* harmony export */ }); /* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); /* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21); @@ -3282,14 +3454,13 @@ __webpack_require__.r(__webpack_exports__); class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { /** - * @typedef {import("../tokeniser.js").Token} Token - * * @param {import("../tokeniser.js").Tokeniser} tokeniser - * @param {Token} base + * @param {import("../tokeniser.js").Token} base * @param {object} [options] - * @param {Token} [options.partial] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, base, { partial } = {}) { + static parse(tokeniser, base, { extMembers = [], partial } = {}) { const tokens = { partial, base }; tokens.mixin = tokeniser.consume("mixin"); if (!tokens.mixin) { @@ -3300,12 +3471,13 @@ class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { new Mixin({ source: tokeniser.source, tokens }), { allowedMembers: [ + ...extMembers, [_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse], [_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier], [_attribute_js__WEBPACK_IMPORTED_MODULE_2__.Attribute.parse, { noInherit: true }], [_operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse, { regular: true }], ], - } + }, ); } @@ -3321,7 +3493,7 @@ class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Dictionary": () => (/* binding */ Dictionary) +/* harmony export */ Dictionary: () => (/* binding */ Dictionary) /* harmony export */ }); /* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); /* harmony import */ var _field_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27); @@ -3332,9 +3504,10 @@ class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, { partial } = {}) { + static parse(tokeniser, { extMembers = [], partial } = {}) { const tokens = { partial }; tokens.base = tokeniser.consume("dictionary"); if (!tokens.base) { @@ -3345,8 +3518,8 @@ class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { new Dictionary({ source: tokeniser.source, tokens }), { inheritable: !partial, - allowedMembers: [[_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]], - } + allowedMembers: [...extMembers, [_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]], + }, ); } @@ -3362,7 +3535,7 @@ class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Field": () => (/* binding */ Field) +/* harmony export */ Field: () => (/* binding */ Field) /* harmony export */ }); /* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); /* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); @@ -3424,7 +3597,7 @@ class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { this.default ? this.default.write(w) : "", w.token(this.tokens.termination), ]), - { data: this, parent } + { data: this, parent }, ); } } @@ -3436,7 +3609,7 @@ class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Namespace": () => (/* binding */ Namespace) +/* harmony export */ Namespace: () => (/* binding */ Namespace) /* harmony export */ }); /* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); /* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); @@ -3455,9 +3628,10 @@ class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, { partial } = {}) { + static parse(tokeniser, { extMembers = [], partial } = {}) { const tokens = { partial }; tokens.base = tokeniser.consume("namespace"); if (!tokens.base) { @@ -3468,11 +3642,12 @@ class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { new Namespace({ source: tokeniser.source, tokens }), { allowedMembers: [ + ...extMembers, [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }], [_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse], [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse, { regular: true }], ], - } + }, ); } @@ -3497,7 +3672,7 @@ for more information.`; message, { autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_4__.autofixAddExposedWindow)(this), - } + }, ); } yield* super.validate(defs); @@ -3511,7 +3686,7 @@ for more information.`; __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "CallbackInterface": () => (/* binding */ CallbackInterface) +/* harmony export */ CallbackInterface: () => (/* binding */ CallbackInterface) /* harmony export */ }); /* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); /* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13); @@ -3523,8 +3698,11 @@ __webpack_require__.r(__webpack_exports__); class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {*} callback + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] */ - static parse(tokeniser, callback, { partial = null } = {}) { + static parse(tokeniser, callback, { extMembers = [] } = {}) { const tokens = { callback }; tokens.base = tokeniser.consume("interface"); if (!tokens.base) { @@ -3534,12 +3712,12 @@ class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Conta tokeniser, new CallbackInterface({ source: tokeniser.source, tokens }), { - inheritable: !partial, allowedMembers: [ + ...extMembers, [_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse], [_operation_js__WEBPACK_IMPORTED_MODULE_1__.Operation.parse, { regular: true }], ], - } + }, ); } @@ -3555,8 +3733,8 @@ class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Conta __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "Writer": () => (/* binding */ Writer), -/* harmony export */ "write": () => (/* binding */ write) +/* harmony export */ Writer: () => (/* binding */ Writer), +/* harmony export */ write: () => (/* binding */ write) /* harmony export */ }); function noop(arg) { return arg; @@ -3640,7 +3818,7 @@ function write(ast, { templates: ts = templates } = {}) { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "validate": () => (/* binding */ validate) +/* harmony export */ validate: () => (/* binding */ validate) /* harmony export */ }); /* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); @@ -3797,14 +3975,14 @@ function validate(ast) { /******/ /************************************************************************/ var __webpack_exports__ = {}; -// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. (() => { __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "WebIDLParseError": () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError), -/* harmony export */ "parse": () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse), -/* harmony export */ "validate": () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate), -/* harmony export */ "write": () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write) +/* harmony export */ WebIDLParseError: () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError), +/* harmony export */ parse: () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse), +/* harmony export */ validate: () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate), +/* harmony export */ write: () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write) /* harmony export */ }); /* harmony import */ var _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); /* harmony import */ var _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index ed006486c28625..2dfd9622ad574b 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -52,7 +52,7 @@ "path": "html/webappapis/timers" }, "interfaces": { - "commit": "e1b27be06b43787a001b7297c4e0fabdd276560f", + "commit": "a8392bd0210378dda13278eeccf2c2602278e214", "path": "interfaces" }, "performance-timeline": { @@ -64,7 +64,7 @@ "path": "resource-timing" }, "resources": { - "commit": "1d2c5fb36a6e477c8f915bde7eca027be6abe792", + "commit": "6a2f32237615d7ea9c1eb1e30453066b5555c603", "path": "resources" }, "streams": { @@ -96,9 +96,13 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "2cb332d71030ba0200610d72b94bb1badf447418", + "commit": "97bbc7247a16231f4744a47a1d9b3d29633d5292", "path": "WebCryptoAPI" }, + "webidl": { + "commit": "63ca529a021fec15b2996db82fd7dd04c11abe85", + "path": "webidl" + }, "webidl/ecmascript-binding/es-exceptions": { "commit": "2f96fa19966d6bc19e979a09479ac8a7aa337c54", "path": "webidl/ecmascript-binding/es-exceptions" diff --git a/test/parallel/test-abortsignal-any.mjs b/test/parallel/test-abortsignal-any.mjs index 19b5569c4779d1..ff5226055b866c 100644 --- a/test/parallel/test-abortsignal-any.mjs +++ b/test/parallel/test-abortsignal-any.mjs @@ -16,7 +16,7 @@ describe('AbortSignal.any()', { concurrency: !process.env.TEST_PARALLEL }, () => () => AbortSignal.any([AbortSignal.abort(), undefined]), { code: 'ERR_INVALID_ARG_TYPE', - message: 'The "signals[1]" argument must be an instance of AbortSignal. Received undefined' + message: 'signals[1] is not of type AbortSignal.', }, ); }); diff --git a/test/parallel/test-blob.js b/test/parallel/test-blob.js index 8ce1c53012ddcc..dd74b244420f2a 100644 --- a/test/parallel/test-blob.js +++ b/test/parallel/test-blob.js @@ -157,6 +157,22 @@ assert.throws(() => new Blob({}), { assert.strictEqual(b.type, ''); } +{ + const b = new Blob(['hello']); + + assert.throws(() => b.slice(1n), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'start is a BigInt and cannot be converted to a number.', + }); + + assert.throws(() => b.slice(0, Symbol()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'end is a Symbol and cannot be converted to a number.', + }); +} + { const b = new Blob([Buffer.from('hello'), Buffer.from('world')]); const mc = new MessageChannel(); diff --git a/test/parallel/test-crypto-argon2.js b/test/parallel/test-crypto-argon2.js index c8015d00458ac1..2137bf345d4ae9 100644 --- a/test/parallel/test-crypto-argon2.js +++ b/test/parallel/test-crypto-argon2.js @@ -95,7 +95,7 @@ const bad = [ ['argon2id', { nonce: nonce.subarray(0, 7) }, 'parameters.nonce.byteLength'], // nonce.byteLength < 8 ['argon2id', { tagLength: 3 }, 'parameters.tagLength'], // tagLength < 4 ['argon2id', { tagLength: 2 ** 32 }, 'parameters.tagLength'], // tagLength > 2^(32)-1 - ['argon2id', { passes: 0 }, 'parameters.passes'], // passes < 2 + ['argon2id', { passes: 0 }, 'parameters.passes'], // passes < 1 ['argon2id', { passes: 2 ** 32 }, 'parameters.passes'], // passes > 2^(32)-1 ['argon2id', { parallelism: 0 }, 'parameters.parallelism'], // parallelism < 1 ['argon2id', { parallelism: 2 ** 24 }, 'parameters.parallelism'], // Parallelism > 2^(24)-1 @@ -103,6 +103,16 @@ const bad = [ ['argon2id', { memory: 2 ** 32 }, 'parameters.memory'], // memory > 2^(32)-1 ]; +{ + const omitted = runArgon2('argon2id', defaults); + const explicitEmpty = runArgon2('argon2id', { + ...defaults, + secret: Buffer.alloc(0), + associatedData: Buffer.alloc(0), + }); + assert.deepStrictEqual(omitted, explicitEmpty); +} + for (const [algorithm, overrides, expected] of good) { const parameters = { ...defaults, ...overrides }; const actual = runArgon2(algorithm, parameters); diff --git a/test/parallel/test-crypto-async-sign-verify.js b/test/parallel/test-crypto-async-sign-verify.js index 9876c4bb6ecd2e..2fb17748324fae 100644 --- a/test/parallel/test-crypto-async-sign-verify.js +++ b/test/parallel/test-crypto-async-sign-verify.js @@ -102,17 +102,19 @@ if (!process.features.openssl_is_boringssl) { // ECDSA w/ ieee-p1363 signature encoding test('ec_secp256k1_public.pem', 'ec_secp256k1_private.pem', 'sha384', false, { dsaEncoding: 'ieee-p1363' }); -} -// DSA w/ der signature encoding -test('dsa_public.pem', 'dsa_private.pem', 'sha256', - false); -test('dsa_public.pem', 'dsa_private.pem', 'sha256', - false, { dsaEncoding: 'der' }); + // DSA w/ der signature encoding + test('dsa_public.pem', 'dsa_private.pem', 'sha256', + false); + test('dsa_public.pem', 'dsa_private.pem', 'sha256', + false, { dsaEncoding: 'der' }); -// DSA w/ ieee-p1363 signature encoding -test('dsa_public.pem', 'dsa_private.pem', 'sha256', false, - { dsaEncoding: 'ieee-p1363' }); + // DSA w/ ieee-p1363 signature encoding + test('dsa_public.pem', 'dsa_private.pem', 'sha256', false, + { dsaEncoding: 'ieee-p1363' }); +} else { + common.printSkipMessage('Skipping unsupported ed448/secp256k1/dsa test cases'); +} // Test Parallel Execution w/ KeyObject is threadsafe in openssl3 { diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index 9778ea548e81d7..2a4e2a1520a353 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -626,22 +626,25 @@ for (const test of TEST_CASES) { { // CCM cipher without data should not crash, see https://github.com/nodejs/node/issues/38035. - const algo = 'aes-128-ccm'; - const key = Buffer.alloc(16); - const iv = Buffer.alloc(12); - const opts = { authTagLength: 10 }; + if (!ciphers.includes('aes-128-ccm')) { + common.printSkipMessage(`unsupported aes-128-ccm test`); + } else { + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + const opts = { authTagLength: 10 }; - const cipher = crypto.createCipheriv(algo, key, iv, opts); - assert.throws(() => { - cipher.final(); - }, hasOpenSSL3 ? { - code: 'ERR_OSSL_TAG_NOT_SET' - } : { - message: /Unsupported state/ - }); + const cipher = crypto.createCipheriv('aes-128-ccm', key, iv, opts); + assert.throws(() => { + cipher.final(); + }, hasOpenSSL3 ? { + code: 'ERR_OSSL_TAG_NOT_SET' + } : { + message: /Unsupported state/ + }); + } } -{ +if (!process.features.openssl_is_boringssl) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -653,11 +656,13 @@ for (const test of TEST_CASES) { message: errMessages.authTagLength }); } +} else { + common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); } // ChaCha20-Poly1305 should respect the authTagLength option and should not // require the authentication tag before calls to update() during decryption. -{ +if (!process.features.openssl_is_boringssl) { const key = Buffer.alloc(32); const iv = Buffer.alloc(12); @@ -697,6 +702,8 @@ for (const test of TEST_CASES) { } } } +} else { + common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); } // ChaCha20-Poly1305 should default to an authTagLength of 16. When encrypting, @@ -706,7 +713,7 @@ for (const test of TEST_CASES) { // shorter tags as long as their length was valid according to NIST SP 800-38D. // For ChaCha20-Poly1305, we intentionally deviate from that because there are // no recommended or approved authentication tag lengths below 16 bytes. -{ +if (!process.features.openssl_is_boringssl) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -740,10 +747,12 @@ for (const test of TEST_CASES) { assert.strictEqual(plaintext.toString('hex'), testCase.plain); } +} else { + common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); } // https://github.com/nodejs/node/issues/45874 -{ +if (!process.features.openssl_is_boringssl) { const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { return algo === 'chacha20-poly1305' && tampered === false; }); @@ -771,10 +780,12 @@ for (const test of TEST_CASES) { assert.throws(() => { decipher.final(); }, /Unsupported state or unable to authenticate data/); +} else { + common.printSkipMessage('Skipping unsupported chacha20-poly1305 test'); } // Refs: https://github.com/nodejs/node/issues/62342 -{ +if (ciphers.includes('aes-128-ccm')) { const key = crypto.randomBytes(16); const nonce = crypto.randomBytes(13); @@ -794,4 +805,6 @@ for (const test of TEST_CASES) { decipher.setAAD(Buffer.alloc(0), { plaintextLength: 0 }); decipher.update(new DataView(new ArrayBuffer(0))); decipher.final(); +} else { + common.printSkipMessage('Skipping unsupported aes-128-ccm test'); } diff --git a/test/parallel/test-crypto-boringssl-evp-list.js b/test/parallel/test-crypto-boringssl-evp-list.js new file mode 100644 index 00000000000000..3f142c24f28a7c --- /dev/null +++ b/test/parallel/test-crypto-boringssl-evp-list.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!process.features.openssl_is_boringssl) + common.skip('BoringSSL-only test'); + +const assert = require('assert'); +const { getCiphers, getHashes } = require('crypto'); + +const ciphers = getCiphers(); +[ + 'aes-128-cbc', + 'aes-256-gcm', + 'des-ede', + 'des-ede-cbc', + 'des-ede3-cbc', + 'rc2-cbc', + 'rc4', +].forEach((cipher) => assert(ciphers.includes(cipher), cipher)); + +const hashes = getHashes(); +[ + 'md4', + 'md5', + 'sha1', + 'sha256', + 'sha512-256', +].forEach((hash) => assert(hashes.includes(hash), hash)); diff --git a/test/parallel/test-crypto-cipheriv-decipheriv.js b/test/parallel/test-crypto-cipheriv-decipheriv.js index 6742722f9e9091..8801ddfe7023fd 100644 --- a/test/parallel/test-crypto-cipheriv-decipheriv.js +++ b/test/parallel/test-crypto-cipheriv-decipheriv.js @@ -62,6 +62,10 @@ function testCipher2(key, iv) { function testCipher3(key, iv) { + if (!crypto.getCiphers().includes('id-aes128-wrap')) { + common.printSkipMessage(`unsupported id-aes128-wrap test`); + return; + } // Test encryption and decryption with explicit key and iv. // AES Key Wrap test vector comes from RFC3394 const plaintext = Buffer.from('00112233445566778899AABBCCDDEEFF', 'hex'); diff --git a/test/parallel/test-crypto-default-shake-lengths-oneshot.js b/test/parallel/test-crypto-default-shake-lengths-oneshot.js index 247e58d93c4303..413f750f8a1b73 100644 --- a/test/parallel/test-crypto-default-shake-lengths-oneshot.js +++ b/test/parallel/test-crypto-default-shake-lengths-oneshot.js @@ -5,6 +5,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('not supported by BoringSSL'); + const { hash } = require('crypto'); common.expectWarning({ diff --git a/test/parallel/test-crypto-dh-curves.js b/test/parallel/test-crypto-dh-curves.js index 81a469c226c261..ddd5ea9377e63f 100644 --- a/test/parallel/test-crypto-dh-curves.js +++ b/test/parallel/test-crypto-dh-curves.js @@ -16,7 +16,9 @@ const p = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' + crypto.createDiffieHellman(p, 'hex'); // Confirm DH_check() results are exposed for optional examination. -const bad_dh = crypto.createDiffieHellman('02', 'hex'); +const bad_dh = process.features.openssl_is_boringssl ? + crypto.createDiffieHellman('abcd', 'hex', 0) : + crypto.createDiffieHellman('02', 'hex'); assert.notStrictEqual(bad_dh.verifyError, 0); const availableCurves = new Set(crypto.getCurves()); diff --git a/test/parallel/test-crypto-dh-group-setters.js b/test/parallel/test-crypto-dh-group-setters.js index 7c774111952ead..37d0a44d0e1e10 100644 --- a/test/parallel/test-crypto-dh-group-setters.js +++ b/test/parallel/test-crypto-dh-group-setters.js @@ -6,6 +6,10 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); +if (process.features.openssl_is_boringssl) { + common.skip('Skipping unsupported Diffie-Hellman tests'); +} + // Unlike DiffieHellman, DiffieHellmanGroup does not have any setters. const dhg = crypto.getDiffieHellman('modp1'); assert.strictEqual(dhg.constructor, crypto.DiffieHellmanGroup); diff --git a/test/parallel/test-crypto-dh-modp2-views.js b/test/parallel/test-crypto-dh-modp2-views.js index 8d01731af79394..a28e615b7f35c7 100644 --- a/test/parallel/test-crypto-dh-modp2-views.js +++ b/test/parallel/test-crypto-dh-modp2-views.js @@ -7,6 +7,10 @@ const assert = require('assert'); const crypto = require('crypto'); const { modp2buf } = require('../common/crypto'); +if (process.features.openssl_is_boringssl) { + common.skip('Skipping unsupported Diffie-Hellman tests'); +} + const modp2 = crypto.createDiffieHellmanGroup('modp2'); const views = common.getArrayBufferViews(modp2buf); diff --git a/test/parallel/test-crypto-dh-modp2.js b/test/parallel/test-crypto-dh-modp2.js index 19767d26f4e5fb..eb262f235ff30b 100644 --- a/test/parallel/test-crypto-dh-modp2.js +++ b/test/parallel/test-crypto-dh-modp2.js @@ -6,6 +6,11 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); const { modp2buf } = require('../common/crypto'); + +if (process.features.openssl_is_boringssl) { + common.skip('Skipping unsupported Diffie-Hellman tests'); +} + const modp2 = crypto.createDiffieHellmanGroup('modp2'); { diff --git a/test/parallel/test-crypto-dh-stateless-async.js b/test/parallel/test-crypto-dh-stateless-async.js deleted file mode 100644 index 9093f735e9ec1b..00000000000000 --- a/test/parallel/test-crypto-dh-stateless-async.js +++ /dev/null @@ -1,220 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasCrypto) - common.skip('missing crypto'); - -const assert = require('assert'); -const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); - -assert.throws(() => crypto.diffieHellman(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), null), { - name: 'TypeError', - code: 'ERR_INVALID_ARG_TYPE', - message: 'The "callback" argument must be of type function. Received null' -}); - -function test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, - { publicKey: bobPublicKey, privateKey: bobPrivateKey }, - expectedValue) { - crypto.diffieHellman({ - privateKey: alicePrivateKey, - publicKey: bobPublicKey - }, common.mustSucceed((buf1) => { - if (expectedValue !== undefined) - assert.deepStrictEqual(buf1, expectedValue); - crypto.diffieHellman({ - privateKey: bobPrivateKey, - publicKey: alicePublicKey - }, common.mustSucceed((buf2) => { - assert.deepStrictEqual(buf1, buf2); - })); - })); -} - -const alicePrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + - 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + - 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + - 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + - '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + - 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwEh82IAVnYNf0Kjb\n' + - 'qYSImDFyg9sH6CJ0GzRK05e6hM3dOSClFYi4kbA7Pr7zyfdn2SH6wSlNS14Jyrtt\n' + - 'HePrRSeYl1T+tk0AfrvaLmyM56F+9B3jwt/nzqr5YxmfVdXb2aQV53VS/mm3pB2H\n' + - 'iIt9FmvFaaOVe2DupqSr6xzbf/zyON+WF5B5HNVOWXswgpgdUsCyygs98hKy/Xje\n' + - 'TGzJUoWInW39t0YgMXenJrkS0m6wol8Rhxx81AGgELNV7EHZqg==\n' + - '-----END PRIVATE KEY-----', - format: 'pem' -}); -const alicePublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBnzCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + - '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + - 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + - 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + - 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxAACgcBR7+iL5qx7aOb9K+aZ\n' + - 'y2oLt7ST33sDKT+nxpag6cWDDWzPBKFDCJ8fr0v7yW453px8N4qi4R7SYYxFBaYN\n' + - 'Y3JvgDg1ct2JC9sxSuUOLqSFn3hpmAjW7cS0kExIVGfdLlYtIqbhhuo45cTEbVIM\n' + - 'rDEz8mjIlnvbWpKB9+uYmbjfVoc3leFvUBqfG2In2m23Md1swsPxr3n7g68H66JX\n' + - 'iBJKZLQMqNdbY14G9rdKmhhTJrQjC+i7Q/wI8JPhOFzHIGA=\n' + - '-----END PUBLIC KEY-----', - format: 'pem' -}); - -const bobPrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + - 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + - 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + - 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + - '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + - 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHxnT7Zw2Ehh1vyw\n' + - 'eolzQFHQzyuT0y+3BF+FxK2Ox7VPguTp57wQfGHbORJ2cwCdLx2mFM7gk4tZ6COS\n' + - 'E3Vta85a/PuhKXNLRdP79JgLnNtVtKXB+ePDS5C2GgXH1RHvqEdJh7JYnMy7Zj4P\n' + - 'GagGtIy3dV5f4FA0B/2C97jQ1pO16ah8gSLQRKsNpTCw2rqsZusE0rK6RaYAef7H\n' + - 'y/0tmLIsHxLIn+WK9CANqMbCWoP4I178BQaqhiOBkNyNZ0ndqA==\n' + - '-----END PRIVATE KEY-----', - format: 'pem' -}); - -const bobPublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + - '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + - 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + - 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + - 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAi26oq8z/GNSBm3zi\n' + - 'gNt7SA7cArUBbTxINa9iLYWp6bxrvCKwDQwISN36/QUw8nUAe8aRyMt0oYn+y6vW\n' + - 'Pw5OlO+TLrUelMVFaADEzoYomH0zVGb0sW4aBN8haC0mbrPt9QshgCvjr1hEPEna\n' + - 'QFKfjzNaJRNMFFd4f2Dn8MSB4yu1xpA1T2i0JSk24vS2H55jx24xhUYtfhT2LJgK\n' + - 'JvnaODey/xtY4Kql10ZKf43Lw6gdQC3G8opC9OxVxt9oNR7Z\n' + - '-----END PUBLIC KEY-----', - format: 'pem' -}); - -const privateKey = Buffer.from( - '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + - 'B891B03B3EBEF3C9F767D921FAC1294D4B5E09CABB6D1DE3EB4527989754FEB64D007EBBDA' + - '2E6C8CE7A17EF41DE3C2DFE7CEAAF963199F55D5DBD9A415E77552FE69B7A41D87888B7D16' + - '6BC569A3957B60EEA6A4ABEB1CDB7FFCF238DF961790791CD54E597B3082981D52C0B2CA0B' + - '3DF212B2FD78DE4C6CC95285889D6DFDB746203177A726B912D26EB0A25F11871C7CD401A0' + - '10B355EC41D9AA', 'hex'); -const publicKey = Buffer.from( - '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + - '0c0848ddfafd0530f275007bc691c8cb74a189fecbabd63f0e4e94ef932eb51e94c5456800' + - 'c4ce8628987d335466f4b16e1a04df21682d266eb3edf50b21802be3af58443c49da40529f' + - '8f335a25134c1457787f60e7f0c481e32bb5c690354f68b4252936e2f4b61f9e63c76e3185' + - '462d7e14f62c980a26f9da3837b2ff1b58e0aaa5d7464a7f8dcbc3a81d402dc6f28a42f4ec' + - '55c6df68351ed9', 'hex'); - -const group = crypto.getDiffieHellman('modp5'); -const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); -dh.setPrivateKey(privateKey); - -// Test simple Diffie-Hellman, no curves involved. -test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, - { publicKey: bobPublicKey, privateKey: bobPrivateKey }, - dh.computeSecret(publicKey)); - -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { group: 'modp5' })); - -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); - -const list = [ - // Same generator, but different primes. - [{ group: 'modp5' }, { group: 'modp18' }]]; - -// TODO(danbev): Take a closer look if there should be a check in OpenSSL3 -// when the dh parameters differ. -if (!hasOpenSSL3) { - // Same primes, but different generator. - list.push([{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }]); - // Same generator, but different primes. - list.push([{ primeLength: 1024 }, { primeLength: 1024 }]); -} - -for (const [params1, params2] of list) { - crypto.diffieHellman({ - privateKey: crypto.generateKeyPairSync('dh', params1).privateKey, - publicKey: crypto.generateKeyPairSync('dh', params2).publicKey - }, common.mustCall((err) => { - assert.ok(err); - assert.strictEqual(err.name, 'Error'); - assert.match(err.message, hasOpenSSL3 ? /mismatching domain parameters/ : /different parameters/); - })); -} - -{ - const privateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + - 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + - 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + - 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + - '7ORbPcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkH\n' + - 'cJaWbWcMNU5KvJgE8XRsCMojcyf//////////wIBAgSBwwKBwHu9fpiqrfJJ+tl9\n' + - 'ujFtEWv4afub6A/1/7sgishOYN3YQ+nmWQlmPpveIY34an5dG82CTrixHwUzQTMF\n' + - 'JaiCW3ax9+qk31f2jTNKrQznmKgopVKXF0FEJC6H79W/8Y0U14gsI9sHpovKhfou\n' + - 'RQD0QogW7ejSwMG8hCYibfrvMm0b5PHlwimISyEKh7VtDQ1frYN/Wr9ZbiV+FePJ\n' + - '2j6RUKYNj1Pv+B4zdMgiLLjILAs8WUfbHciU21KSJh1izVQaUQ==\n' + - '-----END PRIVATE KEY-----' - }); - const publicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + - 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + - '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + - 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + - 'PcIAfLihY78FmNpINhxV05ppFj+o/STPX4NlXSPco62WHGLzViCFUrue1SkHcJaW\n' + - 'bWcMNU5KvJgE8XRsCMojcyf//////////wIBAgOBxQACgcEAmG9LpD8SAA6/W7oK\n' + - 'E4MCuuQtf5E8bqtcEAfYTOOvKyCS+eiX3TtZRsvHJjUBEyeO99PR/KrGVlkSuW52\n' + - 'ZOSXUOFu1L/0tqHrvRVHo+QEq3OvZ3EAyJkdtSEUTztxuUrMOyJXHDc1OUdNSnk0\n' + - 'taGX4mP3247golVx2DS4viDYs7UtaMdx03dWaP6y5StNUZQlgCIUzL7MYpC16V5y\n' + - 'KkFrE+Kp/Z77gEjivaG6YuxVj4GPLxJYbNFVTel42oSVeKuq\n' + - '-----END PUBLIC KEY-----', - format: 'pem' - }); - - // This key combination will result in an unusually short secret, and should - // not cause an assertion failure. - crypto.diffieHellman({ publicKey, privateKey }, common.mustSucceed((secret) => { - assert.strictEqual(secret.toString('hex'), - '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + - '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + - '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + - '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + - '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + - '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + - 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3'); - })); -} - -// Test ECDH. - -test(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), - crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' })); - -crypto.diffieHellman({ - privateKey: crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey, - publicKey: crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }).publicKey -}, common.mustCall((err) => { - assert.ok(err); - assert.strictEqual(err.name, 'Error'); - assert.match(err.message, hasOpenSSL3 ? /mismatching domain parameters/ : /different parameters/); -})); - -test(crypto.generateKeyPairSync('x448'), - crypto.generateKeyPairSync('x448')); - -test(crypto.generateKeyPairSync('x25519'), - crypto.generateKeyPairSync('x25519')); - -{ - const { privateKey } = crypto.generateKeyPairSync('x25519'); - const publicKey = crypto.createPublicKey('-----BEGIN PUBLIC KEY-----\n' + - 'MCowBQYDK2VuAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n' + - '-----END PUBLIC KEY-----'); - crypto.diffieHellman({ publicKey, privateKey }, common.mustCall((err) => { - assert.ok(err); - assert.strictEqual(err.name, 'Error'); - assert.match(err.message, hasOpenSSL3 ? /failed during derivation/ : /Deriving bits failed/); - })); -} diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 84ebbd21aad6a1..2fddaddd2f44ac 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -5,7 +5,25 @@ if (!common.hasCrypto) const assert = require('assert'); const crypto = require('crypto'); -const { hasOpenSSL3 } = require('../common/crypto'); +const { hasOpenSSL } = require('../common/crypto'); +const isBoringSSL = process.features.openssl_is_boringssl; + +// Error code for a key-type mismatch during (EC)DH. The underlying OpenSSL +// error code varies by version, and in OpenSSL 4.0 by platform: some builds +// report a generic internal error instead of a typed key-type mismatch. +// https://github.com/openssl/openssl/issues/30895 +// TODO(panva): Tighten this check once/if fixed. +let keyTypeMismatchCode; +if (hasOpenSSL(4, 0)) { + keyTypeMismatchCode = + /^ERR_(CRYPTO_INCOMPATIBLE_KEY|OSSL_EVP_(OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE|INTERNAL_ERROR))$/; +} else if (hasOpenSSL(3)) { + keyTypeMismatchCode = + /^ERR_(CRYPTO_INCOMPATIBLE_KEY|OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE)$/; +} else { + keyTypeMismatchCode = + /^ERR_(CRYPTO_INCOMPATIBLE_KEY|OSSL_EVP_DIFFERENT_KEY_TYPES)$/; +} assert.throws(() => crypto.diffieHellman(), { name: 'TypeError', @@ -27,6 +45,114 @@ assert.throws(() => crypto.diffieHellman([]), { 'Received an instance of Array', }); +assert.throws(() => crypto.diffieHellman( + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), null), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "callback" argument must be of type function. Received null' +}); + +{ + const kp = { + privateKey: crypto.generateKeySync('aes', { length: 128 }), + publicKey: crypto.generateKeyPairSync('x25519').publicKey, + }; + + assert.throws(() => { + test(kp, kp); + }, { + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type secret, expected private.' + }); +} + +{ + const kp = { + privateKey: crypto.generateKeyPairSync('x25519').publicKey, + publicKey: crypto.generateKeyPairSync('x25519').privateKey, + }; + + assert.throws(() => { + test(kp, kp); + }, { + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type public, expected private.' + }); +} + +{ + const { publicKey: pub } = crypto.generateKeyPairSync('x25519'); + + assert.throws(() => { + crypto.diffieHellman({ + privateKey: pub, + publicKey: pub, + }); + }, { + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type public, expected private.' + }); +} + +{ + const kp = { + privateKey: crypto.generateKeyPairSync('x25519').privateKey, + publicKey: crypto.generateKeySync('aes', { length: 128 }), + }; + + assert.throws(() => { + test(kp, kp); + }, { + code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + message: 'Invalid key object type secret, expected private or public.' + }); +} + +// Test that error messages include the correct property path +{ + const kp = crypto.generateKeyPairSync('x25519'); + const pub = kp.publicKey.export({ type: 'spki', format: 'pem' }); + const priv = kp.privateKey.export({ type: 'pkcs8', format: 'pem' }); + + // Invalid privateKey format + assert.throws(() => crypto.diffieHellman({ + privateKey: { key: Buffer.alloc(0), format: 'banana', type: 'pkcs8' }, + publicKey: pub, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.privateKey\.format/, + }); + + // Invalid privateKey type + assert.throws(() => crypto.diffieHellman({ + privateKey: { key: Buffer.alloc(0), format: 'der', type: 'banana' }, + publicKey: pub, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.privateKey\.type/, + }); + + // Invalid publicKey format + assert.throws(() => crypto.diffieHellman({ + publicKey: { key: Buffer.alloc(0), format: 'banana', type: 'spki' }, + privateKey: priv, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.publicKey\.format/, + }); + + // Invalid publicKey type + assert.throws(() => crypto.diffieHellman({ + publicKey: { key: Buffer.alloc(0), format: 'der', type: 'banana' }, + privateKey: priv, + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /options\.publicKey\.type/, + }); +} + +// Runs diffieHellman for key pairs in both directions, +// verifies results match, and checks both sync and async paths. function test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, { publicKey: bobPublicKey, privateKey: bobPrivateKey }, expectedValue) { @@ -47,10 +173,86 @@ function test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, if (expectedValue !== undefined) assert.deepStrictEqual(buf1, expectedValue); + + // Verify async produces the same results + crypto.diffieHellman({ + privateKey: alicePrivateKey, + publicKey: bobPublicKey + }, common.mustSucceed((asyncBuf) => { + assert.deepStrictEqual(asyncBuf, buf1); + })); + crypto.diffieHellman({ + privateKey: bobPrivateKey, + publicKey: alicePublicKey + }, common.mustSucceed((asyncBuf) => { + assert.deepStrictEqual(asyncBuf, buf1); + })); +} + +// Verifies diffieHellman succeeds sync and async with expected result. +function testDH(options, expected) { + const syncResult = crypto.diffieHellman(options); + if (expected !== undefined) { + assert.deepStrictEqual(syncResult, expected); + } + crypto.diffieHellman(options, common.mustSucceed((asyncResult) => { + assert.deepStrictEqual(asyncResult, syncResult); + })); +} + +function isOpenSSLCodeExpectation(value) { + return String(value).includes('OSSL_'); +} + +// Verifies diffieHellman fails with expected error, both sync and async. +function testDHError(options, expected) { + assert.throws(() => crypto.diffieHellman(options), expected); + const callback = common.mustCall((err) => { + assert.ok(err); + for (const [key, value] of Object.entries(expected)) { + if (key === 'code' && + err.code === undefined && + isOpenSSLCodeExpectation(value)) { + assert.strictEqual(err.name, 'Error'); + assert.strictEqual(typeof err.message, 'string'); + continue; + } + if (value instanceof RegExp) { + assert.match(err[key], value); + } else { + assert.strictEqual(err[key], value); + } + } + }); + try { + crypto.diffieHellman(options, callback); + } catch (err) { + callback(err); + } } -const alicePrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + +{ + const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + + assert.throws(() => crypto.diffieHellman({ privateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + }); + + assert.throws(() => crypto.diffieHellman({ publicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + }); +} + +if (isBoringSSL) { + common.printSkipMessage('Skipping finite-field DH KeyObject import and ' + + 'generation tests unsupported by BoringSSL'); +} else { + const alicePrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -61,10 +263,10 @@ const alicePrivateKey = crypto.createPrivateKey({ 'iIt9FmvFaaOVe2DupqSr6xzbf/zyON+WF5B5HNVOWXswgpgdUsCyygs98hKy/Xje\n' + 'TGzJUoWInW39t0YgMXenJrkS0m6wol8Rhxx81AGgELNV7EHZqg==\n' + '-----END PRIVATE KEY-----', - format: 'pem' -}); -const alicePublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + format: 'pem' + }); + const alicePublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBnzCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -75,11 +277,11 @@ const alicePublicKey = crypto.createPublicKey({ 'rDEz8mjIlnvbWpKB9+uYmbjfVoc3leFvUBqfG2In2m23Md1swsPxr3n7g68H66JX\n' + 'iBJKZLQMqNdbY14G9rdKmhhTJrQjC+i7Q/wI8JPhOFzHIGA=\n' + '-----END PUBLIC KEY-----', - format: 'pem' -}); + format: 'pem' + }); -const bobPrivateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + + const bobPrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -90,11 +292,11 @@ const bobPrivateKey = crypto.createPrivateKey({ 'GagGtIy3dV5f4FA0B/2C97jQ1pO16ah8gSLQRKsNpTCw2rqsZusE0rK6RaYAef7H\n' + 'y/0tmLIsHxLIn+WK9CANqMbCWoP4I178BQaqhiOBkNyNZ0ndqA==\n' + '-----END PRIVATE KEY-----', - format: 'pem' -}); + format: 'pem' + }); -const bobPublicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + const bobPublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -105,79 +307,83 @@ const bobPublicKey = crypto.createPublicKey({ 'QFKfjzNaJRNMFFd4f2Dn8MSB4yu1xpA1T2i0JSk24vS2H55jx24xhUYtfhT2LJgK\n' + 'JvnaODey/xtY4Kql10ZKf43Lw6gdQC3G8opC9OxVxt9oNR7Z\n' + '-----END PUBLIC KEY-----', - format: 'pem' -}); + format: 'pem' + }); -assert.throws(() => crypto.diffieHellman({ privateKey: alicePrivateKey }), { - name: 'TypeError', - code: 'ERR_INVALID_ARG_VALUE', - message: "The property 'options.publicKey' is invalid. Received undefined" -}); + assert.throws(() => crypto.diffieHellman({ privateKey: alicePrivateKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + }); -assert.throws(() => crypto.diffieHellman({ publicKey: alicePublicKey }), { - name: 'TypeError', - code: 'ERR_INVALID_ARG_VALUE', - message: "The property 'options.privateKey' is invalid. Received undefined" -}); + assert.throws(() => crypto.diffieHellman({ publicKey: alicePublicKey }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + }); -const privateKey = Buffer.from( - '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + + const privateKey = Buffer.from( + '487CD880159D835FD0A8DBA9848898317283DB07E822741B344AD397BA84CDDD3920A51588' + 'B891B03B3EBEF3C9F767D921FAC1294D4B5E09CABB6D1DE3EB4527989754FEB64D007EBBDA' + '2E6C8CE7A17EF41DE3C2DFE7CEAAF963199F55D5DBD9A415E77552FE69B7A41D87888B7D16' + '6BC569A3957B60EEA6A4ABEB1CDB7FFCF238DF961790791CD54E597B3082981D52C0B2CA0B' + '3DF212B2FD78DE4C6CC95285889D6DFDB746203177A726B912D26EB0A25F11871C7CD401A0' + '10B355EC41D9AA', 'hex'); -const publicKey = Buffer.from( - '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + + const publicKey = Buffer.from( + '8b6ea8abccff18d4819b7ce280db7b480edc02b5016d3c4835af622d85a9e9bc6bbc22b00d' + '0c0848ddfafd0530f275007bc691c8cb74a189fecbabd63f0e4e94ef932eb51e94c5456800' + 'c4ce8628987d335466f4b16e1a04df21682d266eb3edf50b21802be3af58443c49da40529f' + '8f335a25134c1457787f60e7f0c481e32bb5c690354f68b4252936e2f4b61f9e63c76e3185' + '462d7e14f62c980a26f9da3837b2ff1b58e0aaa5d7464a7f8dcbc3a81d402dc6f28a42f4ec' + '55c6df68351ed9', 'hex'); -const group = crypto.getDiffieHellman('modp5'); -const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); -dh.setPrivateKey(privateKey); - -// Test simple Diffie-Hellman, no curves involved. -test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, - { publicKey: bobPublicKey, privateKey: bobPrivateKey }, - dh.computeSecret(publicKey)); - -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { group: 'modp5' })); - -test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), - crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); + const group = crypto.getDiffieHellman('modp5'); + const dh = crypto.createDiffieHellman(group.getPrime(), group.getGenerator()); + dh.setPrivateKey(privateKey); + + // Test simple Diffie-Hellman, no curves involved. + test({ publicKey: alicePublicKey, privateKey: alicePrivateKey }, + { publicKey: bobPublicKey, privateKey: bobPrivateKey }, + dh.computeSecret(publicKey)); + + test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { group: 'modp5' })); + + test(crypto.generateKeyPairSync('dh', { group: 'modp5' }), + crypto.generateKeyPairSync('dh', { prime: group.getPrime() })); + + // DH parameter mismatch tests + { + const list = [ + // Same generator, but different primes. + [{ group: 'modp5' }, { group: 'modp18' }]]; + + // TODO(danbev): Take a closer look if there should be a check in OpenSSL3 + // when the dh parameters differ. + if (!hasOpenSSL(3)) { + // Same primes, but different generator. + list.push([{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }]); + // Same generator, but different primes. + list.push([{ primeLength: 1024 }, { primeLength: 1024 }]); + } + + for (const [params1, params2] of list) { + const options = { + privateKey: crypto.generateKeyPairSync('dh', params1).privateKey, + publicKey: crypto.generateKeyPairSync('dh', params2).publicKey, + }; + testDHError(options, { + name: 'Error', + code: hasOpenSSL(3) ? + 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' : + 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' + }); + } + } -const list = [ - // Same generator, but different primes. - [{ group: 'modp5' }, { group: 'modp18' }]]; - -// TODO(danbev): Take a closer look if there should be a check in OpenSSL3 -// when the dh parameters differ. -if (!hasOpenSSL3) { - // Same primes, but different generator. - list.push([{ group: 'modp5' }, { prime: group.getPrime(), generator: 5 }]); - // Same generator, but different primes. - list.push([{ primeLength: 1024 }, { primeLength: 1024 }]); -} - -for (const [params1, params2] of list) { - assert.throws(() => { - test(crypto.generateKeyPairSync('dh', params1), - crypto.generateKeyPairSync('dh', params2)); - }, hasOpenSSL3 ? { - name: 'Error', - code: 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' - } : { - name: 'Error', - code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' - }); -} -{ - const privateKey = crypto.createPrivateKey({ - key: '-----BEGIN PRIVATE KEY-----\n' + + // This key combination will result in an unusually short secret, and should + // not cause an assertion failure. + { + const shortPrivateKey = crypto.createPrivateKey({ + key: '-----BEGIN PRIVATE KEY-----\n' + 'MIIBoQIBADCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKL\n' + 'gNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVt\n' + 'bVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR\n' + @@ -188,9 +394,9 @@ for (const [params1, params2] of list) { 'RQD0QogW7ejSwMG8hCYibfrvMm0b5PHlwimISyEKh7VtDQ1frYN/Wr9ZbiV+FePJ\n' + '2j6RUKYNj1Pv+B4zdMgiLLjILAs8WUfbHciU21KSJh1izVQaUQ==\n' + '-----END PRIVATE KEY-----' - }); - const publicKey = crypto.createPublicKey({ - key: '-----BEGIN PUBLIC KEY-----\n' + + }); + const shortPublicKey = crypto.createPublicKey({ + key: '-----BEGIN PUBLIC KEY-----\n' + 'MIIBoDCB1QYJKoZIhvcNAQMBMIHHAoHBAP//////////yQ/aoiFowjTExmKLgNwc\n' + '0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHC\n' + 'ReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7ORb\n' + @@ -201,20 +407,20 @@ for (const [params1, params2] of list) { 'taGX4mP3247golVx2DS4viDYs7UtaMdx03dWaP6y5StNUZQlgCIUzL7MYpC16V5y\n' + 'KkFrE+Kp/Z77gEjivaG6YuxVj4GPLxJYbNFVTel42oSVeKuq\n' + '-----END PUBLIC KEY-----', - format: 'pem' - }); - - // This key combination will result in an unusually short secret, and should - // not cause an assertion failure. - const secret = crypto.diffieHellman({ publicKey, privateKey }); - assert.strictEqual(secret.toString('hex'), - '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + - '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + - '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + - '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + - '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + - '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + - 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3'); + format: 'pem' + }); + + testDH({ publicKey: shortPublicKey, privateKey: shortPrivateKey }, + Buffer.from( + '0099d0fa242af5db9ea7330e23937a27db041f79c581500fc7f9976' + + '554d59d5b9ced934778d72e19a1fefc81e9d981013198748c0b5c6c' + + '762985eec687dc5bec5c9367b05837daee9d0bcc29024ed7f3abba1' + + '2794b65a745117fb0d87bc5b1b2b68c296c3f686cc29e450e4e1239' + + '21f56a5733fe58aabf71f14582954059c2185d342b9b0fa10c2598a' + + '5426c2baee7f9a686fc1e16cd4757c852bf7225a2732250548efe28' + + 'debc26f1acdec51efe23d20786a6f8a14d360803bbc71972e87fd3', + 'hex')); + } } // Test ECDH. @@ -222,83 +428,202 @@ for (const [params1, params2] of list) { test(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' })); -assert.throws(() => { - test(crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), - crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' })); -}, hasOpenSSL3 ? { - name: 'Error', - code: 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' -} : { - name: 'Error', - code: 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' -}); - -test(crypto.generateKeyPairSync('x448'), - crypto.generateKeyPairSync('x448')); - -test(crypto.generateKeyPairSync('x25519'), - crypto.generateKeyPairSync('x25519')); - -assert.throws(() => { - test(crypto.generateKeyPairSync('x448'), - crypto.generateKeyPairSync('x25519')); -}, { - name: 'Error', - code: 'ERR_CRYPTO_INCOMPATIBLE_KEY', - message: 'Incompatible key types for Diffie-Hellman: x448 and x25519' -}); - { - const kp = { - privateKey: crypto.generateKeySync('aes', { length: 128 }), - publicKey: crypto.generateKeyPairSync('x25519').publicKey, + const options = { + privateKey: crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey, + publicKey: crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }).publicKey, }; - - assert.throws(() => { - test(kp, kp); - }, { - code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', - message: 'Invalid key object type secret, expected private.' + testDHError(options, { + name: 'Error', + code: hasOpenSSL(3) ? + 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' : + 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' }); } -{ - const kp = { - privateKey: crypto.generateKeyPairSync('x25519').publicKey, - publicKey: crypto.generateKeyPairSync('x25519').privateKey, - }; - - assert.throws(() => { - test(kp, kp); - }, { - code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', - message: 'Invalid key object type public, expected private.' - }); +if (isBoringSSL) { + common.printSkipMessage('Skipping x448 diffieHellman test cases ' + + 'unsupported by BoringSSL'); +} else { + test(crypto.generateKeyPairSync('x448'), + crypto.generateKeyPairSync('x448')); + + { + const options = { + privateKey: crypto.generateKeyPairSync('x448').privateKey, + publicKey: crypto.generateKeyPairSync('x25519').publicKey, + }; + testDHError(options, { code: keyTypeMismatchCode }); + } } -{ - const kp = { - privateKey: crypto.generateKeyPairSync('x25519').privateKey, - publicKey: crypto.generateKeySync('aes', { length: 128 }), - }; +test(crypto.generateKeyPairSync('x25519'), + crypto.generateKeyPairSync('x25519')); - assert.throws(() => { - test(kp, kp); - }, { - code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', - message: 'Invalid key object type secret, expected private or public.' +// Test all key encoding formats +for (const { privateKey: alicePriv, publicKey: bobPub } of [ + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), + crypto.generateKeyPairSync('x25519'), +]) { + const expected = crypto.diffieHellman({ + privateKey: alicePriv, + publicKey: bobPub, }); + + const encodings = [ + // PEM string + { + privateKey: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + publicKey: bobPub.export({ type: 'spki', format: 'pem' }), + }, + // PEM { key, format } object + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + format: 'pem', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'pem' }), + format: 'pem', + }, + }, + // DER PKCS#8 / SPKI + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'der' }), + format: 'der', + type: 'pkcs8', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'der' }), + format: 'der', + type: 'spki', + }, + }, + // JWK + { + privateKey: { key: alicePriv.export({ format: 'jwk' }), format: 'jwk' }, + publicKey: { key: bobPub.export({ format: 'jwk' }), format: 'jwk' }, + }, + // Raw key material + { + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: alicePriv.asymmetricKeyType, + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType: bobPub.asymmetricKeyType, + ...bobPub.asymmetricKeyDetails, + }, + }, + ]; + + // EC-only encodings + if (alicePriv.asymmetricKeyType === 'ec') { + // DER SEC1 private key + encodings.push({ + privateKey: { + key: alicePriv.export({ type: 'sec1', format: 'der' }), + format: 'der', + type: 'sec1', + }, + publicKey: bobPub, + }); + // Raw with compressed public key + encodings.push({ + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: 'ec', + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public', type: 'compressed' }), + format: 'raw-public', + asymmetricKeyType: 'ec', + ...bobPub.asymmetricKeyDetails, + }, + }); + } + + for (const options of encodings) { + testDH(options, expected); + } } +// Test C++ error conditions (both sync throws and async callback) { - const { privateKey } = crypto.generateKeyPairSync('x25519'); - const publicKey = crypto.createPublicKey('-----BEGIN PUBLIC KEY-----\n' + + const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); + const x448 = isBoringSSL ? null : crypto.generateKeyPairSync('x448'); + const x25519 = crypto.generateKeyPairSync('x25519'); + const ed25519 = crypto.generateKeyPairSync('ed25519'); + + const zeroX25519PublicKey = crypto.createPublicKey('-----BEGIN PUBLIC KEY-----\n' + 'MCowBQYDK2VuAyEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\n' + '-----END PUBLIC KEY-----'); - assert.throws( - () => crypto.diffieHellman({ publicKey, privateKey }), - hasOpenSSL3 ? - { name: 'Error', code: 'ERR_OSSL_FAILED_DURING_DERIVATION' } : - { name: 'Error', message: /Deriving bits failed/ }, - ); + + const encodings = [ + { + privateKey: (key) => key.export({ type: 'pkcs8', format: 'pem' }), + publicKey: (key) => key.export({ type: 'spki', format: 'pem' }), + }, + { + privateKey: (key) => key.export({ type: 'pkcs8', format: 'pem' }), + publicKey: (key) => key, + }, + { + privateKey: (key) => key, + publicKey: (key) => key.export({ type: 'spki', format: 'pem' }), + }, + { + privateKey: (key) => key, + publicKey: (key) => key, + }, + ]; + + for (const { privateKey: privKey, publicKey: pubKey } of encodings) { + // Mismatching EC curves + testDHError({ + privateKey: privKey(ec256.privateKey), + publicKey: pubKey(ec384.publicKey), + }, { code: hasOpenSSL(3) ? + 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' : + 'ERR_OSSL_EVP_DIFFERENT_PARAMETERS' }); + + // Incompatible key types (ec + x25519) + testDHError({ + privateKey: privKey(ec256.privateKey), + publicKey: pubKey(x25519.publicKey), + }, { code: keyTypeMismatchCode }); + + // Unsupported key type (ed25519) + testDHError({ + privateKey: privKey(ed25519.privateKey), + publicKey: pubKey(ed25519.publicKey), + }, { code: hasOpenSSL(4, 0) ? + /^ERR_(CRYPTO_INCOMPATIBLE_KEY|OSSL_EVP_(OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE|INTERNAL_ERROR))$/ : + /^ERR_(CRYPTO_INCOMPATIBLE_KEY|OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE)$/ }); + + if (!isBoringSSL) { + // Incompatible key types (x448 + x25519) + testDHError({ + privateKey: privKey(x448.privateKey), + publicKey: pubKey(x25519.publicKey), + }, { code: keyTypeMismatchCode }); + } + + // Zero x25519 public key + testDHError({ + privateKey: privKey(x25519.privateKey), + publicKey: pubKey(zeroX25519PublicKey), + }, isBoringSSL ? { code: 'ERR_OSSL_EVP_INVALID_PEER_KEY' } : + hasOpenSSL(3) ? + { code: 'ERR_OSSL_FAILED_DURING_DERIVATION' } : + { message: /Deriving bits failed/ }); + } } diff --git a/test/parallel/test-crypto-dh.js b/test/parallel/test-crypto-dh.js index 3c00a5fc73bb9f..8a3dee5b0756d1 100644 --- a/test/parallel/test-crypto-dh.js +++ b/test/parallel/test-crypto-dh.js @@ -8,7 +8,6 @@ const assert = require('assert'); const crypto = require('crypto'); const { hasOpenSSL3, - hasOpenSSL, } = require('../common/crypto'); { @@ -90,13 +89,10 @@ const { } { - // Error message was changed in OpenSSL 3.0.x from 3.0.12, and 3.1.x from 3.1.4. - const hasOpenSSL3WithNewErrorMessage = (hasOpenSSL(3, 0, 12) && !hasOpenSSL(3, 1, 0)) || - (hasOpenSSL(3, 1, 4)); assert.throws(() => { dh3.computeSecret(''); - }, { message: hasOpenSSL3 && !hasOpenSSL3WithNewErrorMessage ? - 'Unspecified validation error' : + }, { message: process.features.openssl_is_boringssl ? + 'Supplied key is invalid' : 'Supplied key is too small' }); } } @@ -104,10 +100,24 @@ const { // Through a fluke of history, g=0 defaults to DH_GENERATOR (2). { const g = 0; - crypto.createDiffieHellman('abcdef', g); + if (process.features.openssl_is_boringssl) { + assert.throws(() => crypto.createDiffieHellman('abcdef', g), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + name: 'Error' + }); + } else { + crypto.createDiffieHellman('abcdef', g); + } crypto.createDiffieHellman('abcdef', 'hex', g); } { - crypto.createDiffieHellman('abcdef', Buffer.from([2])); // OK + if (process.features.openssl_is_boringssl) { + assert.throws(() => crypto.createDiffieHellman('abcdef', Buffer.from([2])), { + code: 'ERR_CRYPTO_OPERATION_FAILED', + name: 'Error' + }); + } else { + crypto.createDiffieHellman('abcdef', Buffer.from([2])); // OK + } } diff --git a/test/parallel/test-crypto-encap-decap.js b/test/parallel/test-crypto-encap-decap.js index 10dc451ef55d68..4b19d71794400c 100644 --- a/test/parallel/test-crypto-encap-decap.js +++ b/test/parallel/test-crypto-encap-decap.js @@ -9,7 +9,9 @@ const fixtures = require('../common/fixtures'); const { hasOpenSSL } = require('../common/crypto'); const { promisify } = require('util'); -if (!hasOpenSSL(3)) { +const isBoringSSL = process.features.openssl_is_boringssl; + +if (!hasOpenSSL(3) && !isBoringSSL) { assert.throws(() => crypto.encapsulate(), { code: 'ERR_CRYPTO_KEM_NOT_SUPPORTED' }); return; } @@ -79,25 +81,25 @@ const keys = { raw: true, }, 'ml-kem-512': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5), // BoringSSL does not support ML-KEM-512 publicKey: fixtures.readKey('ml_kem_512_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_512_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_512_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 768, raw: true, }, 'ml-kem-768': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_768_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_768_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_768_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1088, raw: true, }, 'ml-kem-1024': { - supported: hasOpenSSL(3, 5), + supported: hasOpenSSL(3, 5) || isBoringSSL, publicKey: fixtures.readKey('ml_kem_1024_public.pem', 'ascii'), - privateKey: fixtures.readKey('ml_kem_1024_private.pem', 'ascii'), + privateKey: fixtures.readKey('ml_kem_1024_private_seed_only.pem', 'ascii'), sharedSecretLength: 32, ciphertextLength: 1568, raw: true, @@ -109,7 +111,7 @@ for (const [name, { }] of Object.entries(keys)) { if (!supported) { assert.throws(() => crypto.encapsulate(publicKey), - { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_CRYPTO_OPERATION_FAILED/ }); + { code: /ERR_OSSL_EVP_DECODE_ERROR|ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_CRYPTO_OPERATION_FAILED/ }); continue; } @@ -137,15 +139,11 @@ for (const [name, { publicKey: formatKeyAs(keyObjects.publicKey, { format: 'der', type: 'spki' }), privateKey: formatKeyAs(keyObjects.privateKey, { format: 'der', type: 'pkcs8' }) }, - ]; - - // TODO(@panva): ML-KEM does not have a JWK format defined yet, add once standardized - if (!keyObjects.privateKey.asymmetricKeyType.startsWith('ml')) { - keyPairs.push({ + { publicKey: formatKeyAs(keyObjects.publicKey, { format: 'jwk' }), privateKey: formatKeyAs(keyObjects.privateKey, { format: 'jwk' }) - }); - } + }, + ]; if (raw) { const { asymmetricKeyType } = keyObjects.privateKey; @@ -215,7 +213,7 @@ for (const [name, { } else if (name.startsWith('p-')) { wrongPrivateKey = name === 'p-256' ? keys['p-384'].privateKey : keys['p-256'].privateKey; } else if (name.startsWith('ml-')) { - wrongPrivateKey = name === 'ml-kem-512' ? keys['ml-kem-768'].privateKey : keys['ml-kem-512'].privateKey; + wrongPrivateKey = name === 'ml-kem-768' ? keys['ml-kem-1024'].privateKey : keys['ml-kem-768'].privateKey; } else { wrongPrivateKey = keys.x25519.privateKey; } diff --git a/test/parallel/test-crypto-fips.js b/test/parallel/test-crypto-fips.js index a5e25b8fd1073a..04a95fa5fd37c9 100644 --- a/test/parallel/test-crypto-fips.js +++ b/test/parallel/test-crypto-fips.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support FIPS'); + const assert = require('assert'); const spawnSync = require('child_process').spawnSync; const path = require('path'); diff --git a/test/parallel/test-crypto-getcipherinfo.js b/test/parallel/test-crypto-getcipherinfo.js index b0902908b5c454..d55985aa3c7f6b 100644 --- a/test/parallel/test-crypto-getcipherinfo.js +++ b/test/parallel/test-crypto-getcipherinfo.js @@ -18,6 +18,12 @@ assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined); for (const cipher of ciphers) { const info = getCipherInfo(cipher); + if (process.features.openssl_is_boringssl && !info) { + // BoringSSL reports some legacy ciphers in getCiphers() but returns no + // info for them (e.g. des-ede3, des-ede3-ecb, rc2-40-cbc). + common.printSkipMessage(`Skipping unsupported ${cipher} test case`); + continue; + } assert(info); const info2 = getCipherInfo(info.nid); assert.deepStrictEqual(info, info2); @@ -65,10 +71,14 @@ assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 })); if (!process.features.openssl_is_boringssl) { for (let n = 7; n <= 13; n++) assert(getCipherInfo('aes-128-ccm', { ivLength: n })); +} else { + common.printSkipMessage('Skipping unsupported aes-128-ccm test cases'); } assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 })); if (!process.features.openssl_is_boringssl) { for (let n = 1; n < 16; n++) assert(getCipherInfo('aes-128-ocb', { ivLength: n })); +} else { + common.printSkipMessage('Skipping unsupported aes-128-ocb test cases'); } diff --git a/test/parallel/test-crypto-hash-stream-pipe.js b/test/parallel/test-crypto-hash-stream-pipe.js index d22281abbd5c3c..ac851646a91889 100644 --- a/test/parallel/test-crypto-hash-stream-pipe.js +++ b/test/parallel/test-crypto-hash-stream-pipe.js @@ -30,11 +30,18 @@ const crypto = require('crypto'); const stream = require('stream'); const s = new stream.PassThrough(); -const h = crypto.createHash('sha3-512'); -const expect = '36a38a2a35e698974d4e5791a3f05b05' + - '198235381e864f91a0e8cd6a26b677ec' + - 'dcde8e2b069bd7355fabd68abd6fc801' + - '19659f25e92f8efc961ee3a7c815c758'; +const h = process.features.openssl_is_boringssl ? + crypto.createHash('sha512') : + crypto.createHash('sha3-512'); +const expect = process.features.openssl_is_boringssl ? + 'fba055c6fd0c5b6645407749ed7a8b41' + + 'b8f629f2163c3ca3701d864adabda1f8' + + '93c37bf82b22fdd151ba8e357f611da4' + + '88a74b6a5525dd9b69554c6ce5138ad7' : + '36a38a2a35e698974d4e5791a3f05b05' + + '198235381e864f91a0e8cd6a26b677ec' + + 'dcde8e2b069bd7355fabd68abd6fc801' + + '19659f25e92f8efc961ee3a7c815c758'; s.pipe(h).on('data', common.mustCall(function(c) { assert.strictEqual(c, expect); diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index 8ebe599bbd21ad..00f2240c876400 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -4,6 +4,13 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +common.expectWarning({ + DeprecationWarning: [ + ['crypto.Hash constructor is deprecated.', + 'DEP0179'], + ] +}); + const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); @@ -260,6 +267,8 @@ if (!process.features.openssl_is_boringssl) { assert.throws(() => crypto.createHash('sha256', { outputLength }), { code: 'ERR_OUT_OF_RANGE' }); } +} else { + common.printSkipMessage('Skipping unsupported XOF hash test cases'); } { @@ -280,10 +289,4 @@ if (!process.features.openssl_is_boringssl) { { crypto.Hash('sha256'); - common.expectWarning({ - DeprecationWarning: [ - ['crypto.Hash constructor is deprecated.', - 'DEP0179'], - ] - }); } diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index 5658be6825823f..e0871c2f0cd3fd 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -59,6 +59,47 @@ const { hasOpenSSL } = require('../common/crypto'); } } +// Raw public keys cannot be imported as private keys. +{ + const rawPublicKeys = [ + ['ec', 'ec_p256_public.pem', { namedCurve: 'P-256' }], + ['ed25519', 'ed25519_public.pem'], + ['x25519', 'x25519_public.pem'], + ]; + + if (!process.features.openssl_is_boringssl) { + rawPublicKeys.push( + ['ed448', 'ed448_public.pem'], + ['x448', 'x448_public.pem'], + ); + } else { + common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); + } + + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + rawPublicKeys.push( + ['ml-dsa-44', 'ml_dsa_44_public.pem'], + ['ml-kem-768', 'ml_kem_768_public.pem'], + ); + } + + if (hasOpenSSL(3, 5)) { + rawPublicKeys.push( + ['slh-dsa-sha2-128f', 'slh_dsa_sha2_128f_public.pem'], + ); + } + + for (const [asymmetricKeyType, fixture, options = {}] of rawPublicKeys) { + const publicKey = crypto.createPublicKey(fixtures.readKey(fixture, 'ascii')); + assert.throws(() => crypto.createPrivateKey({ + key: publicKey.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType, + ...options, + }), { code: 'ERR_INVALID_ARG_VALUE' }); + } +} + // Raw seed imports do not support strings. if (hasOpenSSL(3, 5)) { const privKeyObj = crypto.createPrivateKey( @@ -78,10 +119,15 @@ if (hasOpenSSL(3, 5)) { // Key types that don't support raw-* formats { - for (const [type, pub, priv] of [ + const unsupportedKeyTypes = [ ['rsa', 'rsa_public_2048.pem', 'rsa_private_2048.pem'], - ['dsa', 'dsa_public.pem', 'dsa_private.pem'], - ]) { + ]; + if (!process.features.openssl_is_boringssl) { + unsupportedKeyTypes.push(['dsa', 'dsa_public.pem', 'dsa_private.pem']); + } else { + common.printSkipMessage('Skipping unsupported dsa test case'); + } + for (const [type, pub, priv] of unsupportedKeyTypes) { const pubKeyObj = crypto.createPublicKey( fixtures.readKey(pub, 'ascii')); const privKeyObj = crypto.createPrivateKey( @@ -102,7 +148,7 @@ if (hasOpenSSL(3, 5)) { } // DH keys also don't support raw formats - { + if (!process.features.openssl_is_boringssl) { const privKeyObj = crypto.createPrivateKey( fixtures.readKey('dh_private.pem', 'ascii')); assert.throws(() => privKeyObj.export({ format: 'raw-private' }), @@ -111,22 +157,35 @@ if (hasOpenSSL(3, 5)) { for (const format of ['raw-public', 'raw-private', 'raw-seed']) { assert.throws(() => crypto.createPrivateKey({ key: Buffer.alloc(32), format, asymmetricKeyType: 'dh', - }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + }), { + code: format === 'raw-public' ? + 'ERR_INVALID_ARG_VALUE' : + 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', + }); } + } else { + common.printSkipMessage('Skipping unsupported dh test case'); } } // PQC import throws when PQC is not supported if (!hasOpenSSL(3, 5)) { - for (const asymmetricKeyType of [ - 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', - 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', - 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', - ]) { + const unsupported = process.features.openssl_is_boringssl ? + // BoringSSL supports ML-DSA and ML-KEM-{768,1024}, but not ML-KEM-512 or SLH-DSA. + ['ml-kem-512', 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f'] : + [ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', + 'slh-dsa-sha2-128f', 'slh-dsa-shake-128f', + ]; + for (const asymmetricKeyType of unsupported) { for (const format of ['raw-public', 'raw-private', 'raw-seed']) { assert.throws(() => crypto.createPublicKey({ key: Buffer.alloc(32), format, asymmetricKeyType, - }), { code: 'ERR_INVALID_ARG_VALUE' }); + }), { + code: 'ERR_INVALID_ARG_VALUE', + message: /Invalid asymmetricKeyType|Unsupported key type/ + }); } } } @@ -172,27 +231,27 @@ if (!hasOpenSSL(3, 5)) { }), { code: 'ERR_INVALID_ARG_VALUE' }); } -// ML-KEM: -768 and -512 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { - const mlKem512Pub = crypto.createPublicKey( - fixtures.readKey('ml_kem_512_public.pem', 'ascii')); +// ML-KEM: public keys of different type cannot be imported as the other type +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlKem768Pub = crypto.createPublicKey( fixtures.readKey('ml_kem_768_public.pem', 'ascii')); + const mlKem1024Pub = crypto.createPublicKey( + fixtures.readKey('ml_kem_1024_public.pem', 'ascii')); - const mlKem512RawPub = mlKem512Pub.export({ format: 'raw-public' }); const mlKem768RawPub = mlKem768Pub.export({ format: 'raw-public' }); + const mlKem1024RawPub = mlKem1024Pub.export({ format: 'raw-public' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem512RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', + key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-1024', }), { code: 'ERR_INVALID_ARG_VALUE' }); assert.throws(() => crypto.createPublicKey({ - key: mlKem768RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-512', + key: mlKem1024RawPub, format: 'raw-public', asymmetricKeyType: 'ml-kem-768', }), { code: 'ERR_INVALID_ARG_VALUE' }); } // ML-DSA: -44 and -65 public keys cannot be imported as the other type -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsa44Pub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsa65Pub = crypto.createPublicKey( @@ -268,7 +327,12 @@ if (hasOpenSSL(3, 5)) { assert.throws(() => ecPriv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); - for (const type of ['ed25519', 'ed448', 'x25519', 'x448']) { + if (process.features.openssl_is_boringssl) { + common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); + } + for (const type of process.features.openssl_is_boringssl ? + ['ed25519', 'x25519'] : + ['ed25519', 'ed448', 'x25519', 'x448']) { const priv = crypto.createPrivateKey( fixtures.readKey(`${type}_private.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-seed' }), @@ -284,10 +348,10 @@ if (hasOpenSSL(3, 5)) { } // raw-private cannot be used for ml-kem and ml-dsa -if (hasOpenSSL(3, 5)) { - for (const type of ['ml-kem-512', 'ml-dsa-44']) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + for (const type of ['ml-kem-768', 'ml-dsa-44']) { const priv = crypto.createPrivateKey( - fixtures.readKey(`${type.replaceAll('-', '_')}_private.pem`, 'ascii')); + fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } @@ -387,9 +451,9 @@ if (hasOpenSSL(3, 5)) { { code: 'ERR_INVALID_ARG_VALUE' }); // PQC raw-seed -> createPublicKey - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const mlDsaPriv = crypto.createPrivateKey( - fixtures.readKey('ml_dsa_44_private.pem', 'ascii')); + fixtures.readKey('ml_dsa_44_private_seed_only.pem', 'ascii')); const mlDsaPub = crypto.createPublicKey( fixtures.readKey('ml_dsa_44_public.pem', 'ascii')); const mlDsaRawSeed = mlDsaPriv.export({ format: 'raw-seed' }); @@ -436,7 +500,12 @@ if (hasOpenSSL(3, 5)) { // x25519, ed25519, x448, and ed448 cannot be used as 'ec' namedCurve values { - for (const type of ['ed25519', 'x25519', 'ed448', 'x448']) { + if (process.features.openssl_is_boringssl) { + common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); + } + for (const type of process.features.openssl_is_boringssl ? + ['ed25519', 'x25519'] : + ['ed25519', 'x25519', 'ed448', 'x448']) { const priv = crypto.createPrivateKey( fixtures.readKey(`${type}_private.pem`, 'ascii')); const pub = crypto.createPublicKey( diff --git a/test/parallel/test-crypto-key-objects-to-crypto-key.js b/test/parallel/test-crypto-key-objects-to-crypto-key.js index 141e51d1ab74a4..5c3148647324b0 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -29,6 +29,7 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { const algorithms = ['AES-CTR', 'AES-CBC', 'AES-GCM', 'AES-KW']; if (length === 256) algorithms.push('ChaCha20-Poly1305'); + for (const algorithm of algorithms) { const usages = algorithm === 'AES-KW' ? ['wrapKey', 'unwrapKey'] : ['encrypt', 'decrypt']; for (const extractable of [true, false]) { @@ -97,7 +98,15 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { } { - for (const algorithm of ['Ed25519', 'Ed448', 'X25519', 'X448']) { + const algorithms = ['Ed25519', 'X25519']; + + if (!process.features.openssl_is_boringssl) { + algorithms.push('X448', 'Ed448'); + } else { + common.printSkipMessage('Skipping unsupported Ed448/X448 test cases'); + } + + for (const algorithm of algorithms) { const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase()); assert.throws(() => { publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []); @@ -186,7 +195,7 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { const { publicKey, privateKey } = generateKeyPairSync(name.toLowerCase()); assert.throws(() => { diff --git a/test/parallel/test-crypto-key-objects.js b/test/parallel/test-crypto-key-objects.js index 6c1c3fd3afa448..84250a3ada22f2 100644 --- a/test/parallel/test-crypto-key-objects.js +++ b/test/parallel/test-crypto-key-objects.js @@ -246,6 +246,12 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + // Importing a public-only RSA JWK as a private key should fail. + assert.throws( + () => createPrivateKey({ key: publicJwk, format: 'jwk' }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); + const publicDER = publicKey.export({ format: 'der', type: 'pkcs1' @@ -318,6 +324,12 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', createPrivateKey({ key: '' }); }, hasOpenSSL3 ? { message: 'error:1E08010C:DECODER routines::unsupported', + } : process.features.openssl_is_boringssl ? { + message: 'error:0900006e:PEM routines:OPENSSL_internal:NO_START_LINE', + code: 'ERR_OSSL_PEM_NO_START_LINE', + reason: 'NO_START_LINE', + library: 'PEM routines', + function: 'OPENSSL_internal', } : { message: 'error:0909006C:PEM routines:get_name:no start line', code: 'ERR_OSSL_PEM_NO_START_LINE', @@ -331,7 +343,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' }); }, { code: 'ERR_INVALID_ARG_VALUE', - message: "The property 'options.type' is invalid. Received 'spki'" + message: "The property 'key.type' is invalid. Received 'spki'" }); // Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys), @@ -345,13 +357,51 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', }, hasOpenSSL3 ? { message: /error:1E08010C:DECODER routines::unsupported/, library: 'DECODER routines' + } : process.features.openssl_is_boringssl ? { + library: 'public key routines', + message: 'error:06000066:public key routines:OPENSSL_internal:DECODE_ERROR' } : { message: /asn1 encoding/, library: 'asn1 encoding routines' }); } -[ +// Test that createPublicKey/createPrivateKey error messages use 'key.' paths +{ + // createPrivateKey with invalid format + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'banana', type: 'pkcs8' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // createPrivateKey with invalid type + assert.throws(() => { + createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'banana' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); + + // createPublicKey with invalid format + assert.throws(() => { + createPublicKey({ key: Buffer.alloc(0), format: 'banana', type: 'spki' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // createPublicKey with invalid type + assert.throws(() => { + createPublicKey({ key: Buffer.alloc(0), format: 'der', type: 'banana' }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); +} + +for (const info of [ { private: fixtures.readKey('ed25519_private.pem', 'ascii'), public: fixtures.readKey('ed25519_public.pem', 'ascii'), keyType: 'ed25519', @@ -392,9 +442,14 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', 'S0jlSYJk', kty: 'OKP' } }, -].forEach((info) => { +]) { const keyType = info.keyType; + if (process.features.openssl_is_boringssl && keyType.endsWith('448')) { + common.printSkipMessage(`Skipping unsupported ${keyType} test case`); + continue; + } + { const key = createPrivateKey(info.private); assert.strictEqual(key.type, 'private'); @@ -459,9 +514,45 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.deepStrictEqual( importedPub.export({ format: 'raw-public' }), rawPub); } -}); +} + +{ + const okpJwk = { + crv: 'Ed25519', + x: 'K1wIouqnuiA04b3WrMa-xKIKIpfHetNZRv3h9fBf768', + d: 'wVK6M3SMhQh3NK-7GRrSV-BVWQx1FO5pW8hhQeu_NdA', + kty: 'OKP' + }; -[ + // Importing a public-only OKP JWK as a private key should fail. + assert.throws( + () => createPrivateKey({ + key: { kty: okpJwk.kty, crv: okpJwk.crv, x: okpJwk.x }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); + + // Importing an OKP JWK with missing crv should fail. + assert.throws( + () => createPublicKey({ + key: { kty: okpJwk.kty, x: okpJwk.x }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); + + // Importing an OKP JWK with invalid crv should fail. + assert.throws( + () => createPublicKey({ + key: { ...okpJwk, crv: 'invalid' }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); +} + +for (const info of [ { private: fixtures.readKey('ec_p256_private.pem', 'ascii'), public: fixtures.readKey('ec_p256_public.pem', 'ascii'), keyType: 'ec', @@ -509,9 +600,14 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', y: 'Ad3flexBeAfXceNzRBH128kFbOWD6W41NjwKRqqIF26vmgW_8COldGKZjFkOSEASxPB' + 'cvA2iFJRUyQ3whC00j0Np' } }, -].forEach((info) => { +]) { const { keyType, namedCurve } = info; + if (process.features.openssl_is_boringssl && !getCurves().includes(namedCurve)) { + common.printSkipMessage(`Skipping unsupported ${keyType} test case`); + continue; + } + { const key = createPrivateKey(info.private); assert.strictEqual(key.type, 'private'); @@ -591,7 +687,44 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', assert.deepStrictEqual( importedPub.export({ format: 'raw-public' }), rawPub); } -}); +} + +{ + const ecJwk = { + crv: 'P-256', + d: 'DxBsPQPIgMuMyQbxzbb9toew6Ev6e9O6ZhpxLNgmAEo', + kty: 'EC', + x: 'X0mMYR_uleZSIPjNztIkAS3_ud5LhNpbiIFp6fNf2Gs', + y: 'UbJuPy2Xi0lW7UYTBxPK3yGgDu9EAKYIecjkHX5s2lI' + }; + + // Importing a public-only EC JWK as a private key should fail. + assert.throws( + () => createPrivateKey({ + key: { kty: ecJwk.kty, crv: ecJwk.crv, x: ecJwk.x, y: ecJwk.y }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); + + // Importing an EC JWK with missing crv should fail. + assert.throws( + () => createPublicKey({ + key: { kty: ecJwk.kty, x: ecJwk.x, y: ecJwk.y }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_JWK' } + ); + + // Importing an EC JWK with invalid crv should fail. + assert.throws( + () => createPublicKey({ + key: { ...ecJwk, crv: 'invalid' }, + format: 'jwk', + }), + { code: 'ERR_CRYPTO_INVALID_CURVE' } + ); +} { // Reading an encrypted key without a passphrase should fail. @@ -623,7 +756,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', format: 'pem', passphrase: Buffer.alloc(1024, 'a') }), { - message: /bad decrypt/ + message: /bad decrypt|BAD_DECRYPT/ }); const publicKey = createPublicKey(publicDsa); @@ -647,7 +780,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE' }); } -{ +if (!process.features.openssl_is_boringssl) { // Test RSA-PSS. { // This key pair does not restrict the message digest algorithm or salt @@ -843,6 +976,8 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', } } } +} else { + common.printSkipMessage('Skipping unsupported RSA-PSS test case'); } { @@ -942,7 +1077,7 @@ const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem', { const first = generateKeyPairSync('ed25519'); - const second = generateKeyPairSync('ed448'); + const second = generateKeyPairSync('x25519'); assert(!first.publicKey.equals(second.publicKey)); assert(!first.publicKey.equals(second.privateKey)); diff --git a/test/parallel/test-crypto-keygen-async-dsa-key-object.js b/test/parallel/test-crypto-keygen-async-dsa-key-object.js index a3df136230d0f8..ea35facbdc7e20 100644 --- a/test/parallel/test-crypto-keygen-async-dsa-key-object.js +++ b/test/parallel/test-crypto-keygen-async-dsa-key-object.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('not supported by BoringSSL'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-async-dsa.js b/test/parallel/test-crypto-keygen-async-dsa.js index 41968d8cc23365..d7c857d35e214b 100644 --- a/test/parallel/test-crypto-keygen-async-dsa.js +++ b/test/parallel/test-crypto-keygen-async-dsa.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('not supported by BoringSSL'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js index bddb4aa2fbdcd6..b0945dcc83a2cd 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk-ec.js @@ -11,7 +11,11 @@ const { // Test async elliptic curve key generation with 'jwk' encoding and named // curve. -['P-384', 'P-256', 'P-521', 'secp256k1'].forEach((curve) => { +for (const curve of ['P-384', 'P-256', 'P-521', 'secp256k1']) { + if (process.features.openssl_is_boringssl && curve === 'secp256k1') { + common.printSkipMessage(`Skipping unsupported ${curve} test case`); + continue; + } generateKeyPair('ec', { namedCurve: curve, publicKeyEncoding: { @@ -32,4 +36,4 @@ const { assert.strictEqual(publicKey.crv, curve); assert.strictEqual(publicKey.crv, privateKey.crv); })); -}); +}; diff --git a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js index 5243edd8c825b7..731960b0d56a06 100644 --- a/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js +++ b/test/parallel/test-crypto-keygen-async-elliptic-curve-jwk.js @@ -11,12 +11,11 @@ const { // Test async elliptic curve key generation with 'jwk' encoding. { - [ - 'ed25519', - 'ed448', - 'x25519', - 'x448', - ].forEach((type) => { + for (const type of ['ed25519', 'ed448', 'x25519', 'x448']) { + if (process.features.openssl_is_boringssl && type.endsWith('448')) { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } generateKeyPair(type, { publicKeyEncoding: { format: 'jwk' @@ -36,5 +35,5 @@ const { assert.strictEqual(publicKey.crv, expectedCrv); assert.strictEqual(publicKey.crv, privateKey.crv); })); - }); + } } diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js index 55aa3831c4233b..246cbe5dd1ace1 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support paramEncoding: explicit'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js index 8a55d4338bc72f..c3b8ab6e8f5093 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support paramEncoding: explicit'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js index 46223f08d7445a..8084cdfc0b3bf1 100644 --- a/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js +++ b/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support paramEncoding: explicit'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-bit-length.js b/test/parallel/test-crypto-keygen-bit-length.js index 63a80659bb2f53..13234589a5d6de 100644 --- a/test/parallel/test-crypto-keygen-bit-length.js +++ b/test/parallel/test-crypto-keygen-bit-length.js @@ -4,6 +4,10 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support arbitrary RSA modulus length ' + + 'or RSA-PSS/DSA key generation'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-dh-classic.js b/test/parallel/test-crypto-keygen-dh-classic.js index ecf5ce7863b8a4..44af7730126afe 100644 --- a/test/parallel/test-crypto-keygen-dh-classic.js +++ b/test/parallel/test-crypto-keygen-dh-classic.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support DH key pair generation'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-eddsa.js b/test/parallel/test-crypto-keygen-eddsa.js index 5a097c2524f3ea..0a132235ea28ba 100644 --- a/test/parallel/test-crypto-keygen-eddsa.js +++ b/test/parallel/test-crypto-keygen-eddsa.js @@ -11,17 +11,19 @@ const { // Test EdDSA key generation. { - if (!/^1\.1\.0/.test(process.versions.openssl)) { - ['ed25519', 'ed448', 'x25519', 'x448'].forEach((keyType) => { - generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { - assert.strictEqual(publicKey.type, 'public'); - assert.strictEqual(publicKey.asymmetricKeyType, keyType); - assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); + for (const keyType of ['ed25519', 'ed448', 'x25519', 'x448']) { + if (process.features.openssl_is_boringssl && keyType.endsWith('448')) { + common.printSkipMessage(`Skipping unsupported ${keyType} test case`); + continue; + } + generateKeyPair(keyType, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(publicKey.asymmetricKeyType, keyType); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); - assert.strictEqual(privateKey.type, 'private'); - assert.strictEqual(privateKey.asymmetricKeyType, keyType); - assert.deepStrictEqual(privateKey.asymmetricKeyDetails, {}); - })); - }); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(privateKey.asymmetricKeyType, keyType); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, {}); + })); } } diff --git a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js index b5ff5dc2059c1b..9086e2e8a5f1ba 100644 --- a/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js +++ b/test/parallel/test-crypto-keygen-invalid-parameter-encoding-dsa.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support DSA key pair generation'); + const assert = require('assert'); const { diff --git a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js index 97dafe1be3cbd0..559c6f0af05163 100644 --- a/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js +++ b/test/parallel/test-crypto-keygen-no-rsassa-pss-params.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support RSA-PSS key pair generation'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-raw.js b/test/parallel/test-crypto-keygen-raw.js index fd2971dc2c86cd..e55c3f10eed8e3 100644 --- a/test/parallel/test-crypto-keygen-raw.js +++ b/test/parallel/test-crypto-keygen-raw.js @@ -158,12 +158,14 @@ const { hasOpenSSL } = require('../common/crypto'); } // Test error: raw with DSA. -{ +if (!process.features.openssl_is_boringssl) { assert.throws(() => generateKeyPairSync('dsa', { modulusLength: 2048, publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); +} else { + common.printSkipMessage(`Skipping unsupported dsa test case`); } // Test error: raw-private in publicKeyEncoding. @@ -203,7 +205,7 @@ const { hasOpenSSL } = require('../common/crypto'); } // PQC key types -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // Test raw encoding for ML-DSA key types (raw-public + raw-seed only). { for (const type of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { @@ -230,6 +232,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for ML-KEM key types (raw-public + raw-seed only). { for (const type of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { + if (process.features.openssl_is_boringssl && type === 'ml-kem-512') { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, @@ -244,7 +250,7 @@ if (hasOpenSSL(3, 5)) { // Test error: raw-private with ML-KEM (not supported). { - assert.throws(() => generateKeyPairSync('ml-kem-512', { + assert.throws(() => generateKeyPairSync('ml-kem-768', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); @@ -253,6 +259,10 @@ if (hasOpenSSL(3, 5)) { // Test raw encoding for SLH-DSA key types. { for (const type of ['slh-dsa-sha2-128f', 'slh-dsa-shake-128f']) { + if (process.features.openssl_is_boringssl) { + common.printSkipMessage(`Skipping unsupported ${type} test case`); + continue; + } const { publicKey, privateKey } = generateKeyPairSync(type, { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-private' }, @@ -264,11 +274,13 @@ if (hasOpenSSL(3, 5)) { } // Test error: raw-seed with SLH-DSA (not supported). - { + if (!process.features.openssl_is_boringssl) { assert.throws(() => generateKeyPairSync('slh-dsa-sha2-128f', { publicKeyEncoding: { format: 'raw-public' }, privateKeyEncoding: { format: 'raw-seed' }, }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + } else { + common.printSkipMessage('Skipping unsupported slh-dsa test case'); } // Test async generateKeyPair with raw encoding for PQC types. diff --git a/test/parallel/test-crypto-keygen-rfc8017-9-1.js b/test/parallel/test-crypto-keygen-rfc8017-9-1.js index 7198be1c41343b..fbefb1b4f642b4 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-9-1.js +++ b/test/parallel/test-crypto-keygen-rfc8017-9-1.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support RSA-PSS key pair generation'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js index f87dcf749bf6d0..bc96d57ed0cd1b 100644 --- a/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js +++ b/test/parallel/test-crypto-keygen-rfc8017-a-2-3.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support RSA-PSS key pair generation'); + const assert = require('assert'); const { generateKeyPair, diff --git a/test/parallel/test-crypto-keygen-rsa-pss.js b/test/parallel/test-crypto-keygen-rsa-pss.js index 41ebec97a5d2dd..3ce0d40e8d1b2d 100644 --- a/test/parallel/test-crypto-keygen-rsa-pss.js +++ b/test/parallel/test-crypto-keygen-rsa-pss.js @@ -4,6 +4,9 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) + common.skip('BoringSSL does not support RSA-PSS key pair generation'); + const assert = require('assert'); const { constants, diff --git a/test/parallel/test-crypto-keygen.js b/test/parallel/test-crypto-keygen.js index a96730e0181094..7dddfcab275d16 100644 --- a/test/parallel/test-crypto-keygen.js +++ b/test/parallel/test-crypto-keygen.js @@ -15,6 +15,7 @@ const { const { inspect } = require('util'); const { hasOpenSSL3 } = require('../common/crypto'); +const isBoringSSL = process.features.openssl_is_boringssl; // Test invalid parameter encoding. { @@ -361,13 +362,24 @@ const { hasOpenSSL3 } = require('../common/crypto'); // Test invalid exponents. (caught by OpenSSL) for (const publicExponent of [1, 1 + 0x10001]) { - generateKeyPair('rsa', { - modulusLength: 4096, - publicExponent - }, common.mustCall((err) => { - assert.strictEqual(err.name, 'Error'); - assert.match(err.message, hasOpenSSL3 ? /exponent/ : /bad e value/); - })); + if (isBoringSSL) { + assert.throws(() => generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustNotCall()), { + name: 'RangeError', + code: 'ERR_OUT_OF_RANGE', + message: 'publicExponent is invalid', + }); + } else { + generateKeyPair('rsa', { + modulusLength: 4096, + publicExponent + }, common.mustCall((err) => { + assert.strictEqual(err.name, 'Error'); + assert.match(err.message, hasOpenSSL3 ? /exponent/ : /bad e value/); + })); + } } } @@ -494,16 +506,21 @@ const { hasOpenSSL3 } = require('../common/crypto'); }); })); - generateKeyPair('ec', { - namedCurve: 'secp256k1', - }, common.mustSucceed((publicKey, privateKey) => { - assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { - namedCurve: 'secp256k1' - }); - assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { - namedCurve: 'secp256k1' - }); - })); + if (isBoringSSL) { + common.printSkipMessage('Skipping secp256k1 keygen test case ' + + 'unsupported by BoringSSL'); + } else { + generateKeyPair('ec', { + namedCurve: 'secp256k1', + }, common.mustSucceed((publicKey, privateKey) => { + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + assert.deepStrictEqual(privateKey.asymmetricKeyDetails, { + namedCurve: 'secp256k1' + }); + })); + } } { diff --git a/test/parallel/test-crypto-keyobject-brand-check.js b/test/parallel/test-crypto-keyobject-brand-check.js new file mode 100644 index 00000000000000..ac0cf1b65f709b --- /dev/null +++ b/test/parallel/test-crypto-keyobject-brand-check.js @@ -0,0 +1,96 @@ +'use strict'; + +// KeyObject instances are backed by NativeKeyObject and must be +// recognized by native brand, not by public prototype shape or +// forgeable own properties. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { + createHmac, + createSecretKey, + generateKeyPairSync, + KeyObject, +} = require('node:crypto'); +const { types: { isKeyObject } } = require('node:util'); + +const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + +function getter(proto, name) { + return Object.getOwnPropertyDescriptor(proto, name).get; +} + +{ + const secret = createSecretKey(Buffer.alloc(16)); + const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 1024 }); + + const type = getter(KeyObject.prototype, 'type'); + const symmetricKeySize = + getter(Object.getPrototypeOf(secret), 'symmetricKeySize'); + const asymmetricProto = Object.getPrototypeOf(Object.getPrototypeOf(publicKey)); + const asymmetricKeyType = getter(asymmetricProto, 'asymmetricKeyType'); + const asymmetricKeyDetails = getter(asymmetricProto, 'asymmetricKeyDetails'); + + assert.strictEqual(isKeyObject(secret), true); + assert.strictEqual(isKeyObject(publicKey), true); + assert.strictEqual(Object.hasOwn(KeyObject, 'getSlots'), false); + for (const key of [secret, publicKey]) { + for (let proto = Object.getPrototypeOf(key); + proto !== null; + proto = Object.getPrototypeOf(proto)) { + assert.strictEqual(Object.hasOwn(proto, 'getSlots'), false); + assert.strictEqual('getSlots' in proto, false); + if (Object.hasOwn(proto, 'constructor')) { + assert.strictEqual(Object.hasOwn(proto.constructor, 'getSlots'), false); + assert.strictEqual(proto.constructor.getSlots, undefined); + } + } + } + + for (const value of [{}, { __proto__: null }, 1, null, undefined, + Buffer.alloc(1), function() {}]) { + assert.throws(() => type.call(value), invalidThis); + assert.throws(() => symmetricKeySize.call(value), invalidThis); + assert.throws(() => asymmetricKeyType.call(value), invalidThis); + assert.throws(() => asymmetricKeyDetails.call(value), invalidThis); + } + + assert.throws(() => symmetricKeySize.call(publicKey), invalidThis); + assert.throws(() => asymmetricKeyType.call(secret), invalidThis); + assert.throws(() => asymmetricKeyDetails.call(secret), invalidThis); + + const spoofed = {}; + Object.setPrototypeOf(spoofed, Object.getPrototypeOf(secret)); + assert.strictEqual(spoofed instanceof KeyObject, true); + assert.strictEqual(isKeyObject(spoofed), false); + assert.throws(() => type.call(spoofed), invalidThis); + assert.throws(() => symmetricKeySize.call(spoofed), invalidThis); + assert.throws(() => createHmac('sha256', spoofed), { + code: 'ERR_INVALID_ARG_TYPE', + }); + + const originalHasInstance = + Object.getOwnPropertyDescriptor(KeyObject, Symbol.hasInstance); + Object.defineProperty(KeyObject, Symbol.hasInstance, { + configurable: true, + value: () => true, + }); + try { + const buf = Buffer.alloc(16); + assert.strictEqual(buf instanceof KeyObject, true); + assert.strictEqual(isKeyObject(buf), false); + assert.throws(() => type.call(buf), invalidThis); + assert.throws(() => symmetricKeySize.call(buf), invalidThis); + assert.throws(() => asymmetricKeyType.call(buf), invalidThis); + assert.throws(() => asymmetricKeyDetails.call(buf), invalidThis); + } finally { + if (originalHasInstance === undefined) { + delete KeyObject[Symbol.hasInstance]; + } else { + Object.defineProperty(KeyObject, Symbol.hasInstance, originalHasInstance); + } + } +} diff --git a/test/parallel/test-crypto-keyobject-clone-transfer.js b/test/parallel/test-crypto-keyobject-clone-transfer.js new file mode 100644 index 00000000000000..1d68e4b9911a0a --- /dev/null +++ b/test/parallel/test-crypto-keyobject-clone-transfer.js @@ -0,0 +1,138 @@ +'use strict'; + +// KeyObject instances must survive structured cloning with their +// native backing data and hidden JS slot semantics preserved. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { once } = require('node:events'); +const { + createHmac, + createSecretKey, + generateKeyPairSync, + sign, + verify, +} = require('node:crypto'); +const { MessageChannel, Worker } = require('node:worker_threads'); +const { types: { isKeyObject } } = require('node:util'); + +function assertNoOwnKeys(key) { + assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []); + assert.deepStrictEqual(Object.getOwnPropertyNames(key), []); + assert.deepStrictEqual(Reflect.ownKeys(key), []); +} + +function assertSameKeyObject(original, clone) { + assert.notStrictEqual(original, clone); + assert.strictEqual(isKeyObject(clone), true); + assert.strictEqual(original.type, clone.type); + assert.strictEqual(original.equals(clone), true); + assert.deepStrictEqual(original, clone); + if (clone.type === 'secret') { + assert.strictEqual(original.symmetricKeySize, clone.symmetricKeySize); + } else { + assert.strictEqual(original.asymmetricKeyType, clone.asymmetricKeyType); + assert.deepStrictEqual( + original.asymmetricKeyDetails, + clone.asymmetricKeyDetails); + } + assertNoOwnKeys(original); + assertNoOwnKeys(clone); +} + +async function roundTripViaMessageChannel(key) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(key); + const [received] = await once(port2, 'message'); + port1.close(); + port2.close(); + return received; +} + +async function roundTripViaWorker(key) { + const worker = new Worker(` + 'use strict'; + const { parentPort } = require('node:worker_threads'); + const { types: { isKeyObject } } = require('node:util'); + + parentPort.once('message', ({ key, expectedType }) => { + try { + if (!isKeyObject(key) || key.type !== expectedType) { + throw new Error('KeyObject slot mismatch in worker'); + } + parentPort.postMessage({ key }); + } catch (err) { + parentPort.postMessage({ error: err.stack || err.message }); + } + }); + `, { eval: true }); + + worker.postMessage({ key, expectedType: key.type }); + const [msg] = await once(worker, 'message'); + await worker.terminate(); + + assert.strictEqual(msg.error, undefined, msg.error); + return msg.key; +} + +function hmacDigest(key) { + return createHmac('sha256', key).update('payload').digest('hex'); +} + +(async () => { + const secret = createSecretKey(Buffer.alloc(16)); + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + modulusLength: 1024, + }); + + for (const key of [secret, publicKey, privateKey]) { + const cloned = structuredClone(key); + assertSameKeyObject(key, cloned); + + const viaPort = await roundTripViaMessageChannel(key); + assertSameKeyObject(key, viaPort); + + const clonedAgain = structuredClone(viaPort); + assertSameKeyObject(key, clonedAgain); + + const viaWorker = await roundTripViaWorker(key); + assertSameKeyObject(key, viaWorker); + } + + const secretClones = [ + secret, + structuredClone(secret), + await roundTripViaMessageChannel(secret), + await roundTripViaWorker(secret), + ]; + const digest = hmacDigest(secret); + for (const key of secretClones) { + assert.strictEqual(hmacDigest(key), digest); + } + + const data = Buffer.from('payload'); + const publicClones = [ + publicKey, + structuredClone(publicKey), + await roundTripViaMessageChannel(publicKey), + await roundTripViaWorker(publicKey), + ]; + const privateClones = [ + privateKey, + structuredClone(privateKey), + await roundTripViaMessageChannel(privateKey), + await roundTripViaWorker(privateKey), + ]; + + const signature = sign('sha256', data, privateKey); + for (const key of publicClones) { + assert.strictEqual(verify('sha256', data, key, signature), true); + } + for (const key of privateClones) { + const clonedSignature = sign('sha256', data, key); + assert.strictEqual(verify('sha256', data, publicKey, clonedSignature), true); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-crypto-keyobject-hidden-slots.js b/test/parallel/test-crypto-keyobject-hidden-slots.js new file mode 100644 index 00000000000000..1ea243ba0ab818 --- /dev/null +++ b/test/parallel/test-crypto-keyobject-hidden-slots.js @@ -0,0 +1,213 @@ +'use strict'; + +// KeyObject public getters and methods are configurable JS properties. +// Internal consumers must read key state through hidden native-backed +// slots, not through user-replaceable accessors. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { + createCipheriv, + createDecipheriv, + createHmac, + createPrivateKey, + createPublicKey, + createSecretKey, + createSign, + createVerify, + diffieHellman, + generateKeyPairSync, + hkdfSync, + KeyObject, + privateDecrypt, + publicEncrypt, + sign, + verify, + X509Certificate, +} = require('node:crypto'); +const { readFileSync } = require('node:fs'); +const fixtures = require('../common/fixtures'); + +function updateFinal(cipher, data = Buffer.alloc(16)) { + return Buffer.concat([cipher.update(data), cipher.final()]); +} + +{ + const secret = createSecretKey(Buffer.alloc(16)); + const secretProto = Object.getPrototypeOf(secret); + const originalType = + Object.getOwnPropertyDescriptor(KeyObject.prototype, 'type'); + const originalSize = + Object.getOwnPropertyDescriptor(secretProto, 'symmetricKeySize'); + + Object.defineProperty(KeyObject.prototype, 'type', { + configurable: true, + get() { return 'public'; }, + }); + Object.defineProperty(secretProto, 'symmetricKeySize', { + configurable: true, + get() { return 1; }, + }); + + try { + assert.strictEqual(secret.type, 'public'); + assert.strictEqual(secret.symmetricKeySize, 1); + + assert.strictEqual( + createHmac('sha256', secret).update('payload').digest('hex').length, + 64); + + const ciphertext = updateFinal( + createCipheriv('aes-128-ecb', secret, null)); + const plaintext = updateFinal( + createDecipheriv('aes-128-ecb', secret, null), ciphertext); + assert.deepStrictEqual(plaintext, Buffer.alloc(16)); + + assert.strictEqual( + hkdfSync('sha256', secret, Buffer.alloc(0), Buffer.alloc(0), 16) + .byteLength, + 16); + + const cryptoKey = secret.toCryptoKey( + { name: 'AES-GCM' }, true, ['encrypt']); + assert.strictEqual(cryptoKey.algorithm.length, 128); + } finally { + Object.defineProperty(KeyObject.prototype, 'type', originalType); + Object.defineProperty(secretProto, 'symmetricKeySize', originalSize); + } +} + +{ + const { + privateKey: ecPrivateKey, + publicKey, + } = generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const asymmetricProto = Object.getPrototypeOf(Object.getPrototypeOf(publicKey)); + const originalAsymmetricKeyType = + Object.getOwnPropertyDescriptor(asymmetricProto, 'asymmetricKeyType'); + + Object.defineProperty(asymmetricProto, 'asymmetricKeyType', { + configurable: true, + get() { return 'rsa'; }, + }); + + try { + assert.strictEqual(publicKey.asymmetricKeyType, 'rsa'); + assert.strictEqual( + publicKey.export({ format: 'raw-public', type: 'compressed' }).length, + 33); + + assert.strictEqual( + diffieHellman({ privateKey: ecPrivateKey, publicKey }).byteLength, + 32); + } finally { + Object.defineProperty( + asymmetricProto, 'asymmetricKeyType', originalAsymmetricKeyType); + } +} + +{ + const { publicKey } = generateKeyPairSync('rsa', { + modulusLength: 1024, + }); + + const details = publicKey.asymmetricKeyDetails; + assert.strictEqual(details.modulusLength, 1024); + assert.strictEqual(details.publicExponent, 65537n); + + details.modulusLength = 1; + details.publicExponent = 3n; + details.extra = true; + + const freshDetails = publicKey.asymmetricKeyDetails; + assert.notStrictEqual(freshDetails, details); + assert.strictEqual(freshDetails.modulusLength, 1024); + assert.strictEqual(freshDetails.publicExponent, 65537n); + assert.strictEqual(freshDetails.extra, undefined); +} + +{ + Object.defineProperty(Object.prototype, 'publicExponent', { + configurable: true, + value: new Uint8Array([1, 0, 1]), + }); + + try { + const { publicKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' }); + assert.deepStrictEqual(publicKey.asymmetricKeyDetails, { + namedCurve: 'prime256v1', + }); + assert.strictEqual( + Object.hasOwn(publicKey.asymmetricKeyDetails, 'publicExponent'), + false); + } finally { + delete Object.prototype.publicExponent; + } +} + +{ + const { privateKey, publicKey } = generateKeyPairSync('rsa', { + modulusLength: 1024, + }); + const originalType = + Object.getOwnPropertyDescriptor(KeyObject.prototype, 'type'); + const data = Buffer.from('payload'); + + Object.defineProperty(KeyObject.prototype, 'type', { + configurable: true, + get() { return 'secret'; }, + }); + + try { + assert.strictEqual(privateKey.type, 'secret'); + assert.strictEqual(publicKey.type, 'secret'); + + const signature = sign('sha256', data, privateKey); + assert.strictEqual(verify('sha256', data, publicKey, signature), true); + + const signer = createSign('sha256'); + signer.update(data); + const streamSignature = signer.sign(privateKey); + const verifier = createVerify('sha256'); + verifier.update(data); + assert.strictEqual(verifier.verify(publicKey, streamSignature), true); + + const ciphertext = publicEncrypt(publicKey, data); + assert.deepStrictEqual(privateDecrypt(privateKey, ciphertext), data); + + assert.strictEqual(publicKey.equals(createPublicKey(privateKey)), true); + + const x509 = new X509Certificate( + readFileSync(fixtures.path('keys', 'agent1-cert.pem'))); + const x509PrivateKey = createPrivateKey( + readFileSync(fixtures.path('keys', 'agent1-key.pem'))); + const ca = new X509Certificate( + readFileSync(fixtures.path('keys', 'ca1-cert.pem'))); + + assert.strictEqual(x509.checkPrivateKey(x509PrivateKey), true); + assert.strictEqual(x509.verify(ca.publicKey), true); + } finally { + Object.defineProperty(KeyObject.prototype, 'type', originalType); + } +} + +{ + const a = createSecretKey(Buffer.alloc(16)); + const b = createSecretKey(Buffer.alloc(16, 1)); + const originalEquals = + Object.getOwnPropertyDescriptor(KeyObject.prototype, 'equals'); + + Object.defineProperty(KeyObject.prototype, 'equals', { + configurable: true, + value: () => true, + }); + + try { + assert.notDeepStrictEqual(a, b); + } finally { + Object.defineProperty(KeyObject.prototype, 'equals', originalEquals); + } +} diff --git a/test/parallel/test-crypto-keyobject-no-own-symbols.js b/test/parallel/test-crypto-keyobject-no-own-symbols.js new file mode 100644 index 00000000000000..f1539c6a0f7ab5 --- /dev/null +++ b/test/parallel/test-crypto-keyobject-no-own-symbols.js @@ -0,0 +1,42 @@ +'use strict'; + +// KeyObject instances must not expose own string or Symbol properties, even +// after the native slot tuple and lazy metadata cache have been populated. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { + createSecretKey, + generateKeyPairSync, +} = require('node:crypto'); + +function assertNoOwnKeys(key) { + assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []); + assert.deepStrictEqual(Object.getOwnPropertyNames(key), []); + assert.deepStrictEqual(Reflect.ownKeys(key), []); +} + +{ + const secret = createSecretKey(Buffer.alloc(16)); + const { publicKey, privateKey } = generateKeyPairSync('rsa', { + modulusLength: 1024, + }); + + for (const key of [secret, publicKey, privateKey]) { + const type = key.type; + assert.notStrictEqual(type, undefined); + if (type === 'secret') { + assert.strictEqual(key.symmetricKeySize, 16); + key.export(); + } else { + assert.notStrictEqual(key.asymmetricKeyType, undefined); + assert.notStrictEqual(key.asymmetricKeyDetails, undefined); + key.export({ format: 'pem', type: 'pkcs1' }); + } + key.equals(key); + assertNoOwnKeys(key); + } +} diff --git a/test/parallel/test-crypto-oneshot-hash-xof.js b/test/parallel/test-crypto-oneshot-hash-xof.js index 75cb4800ff1bd5..b4363c31592763 100644 --- a/test/parallel/test-crypto-oneshot-hash-xof.js +++ b/test/parallel/test-crypto-oneshot-hash-xof.js @@ -7,6 +7,10 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); const crypto = require('crypto'); +if (process.features.openssl_is_boringssl) { + common.skip('BoringSSL does not support XOF hash functions'); +} + // Test XOF hash functions and the outputLength option. { // Default outputLengths. diff --git a/test/parallel/test-crypto-pqc-encrypted-pkcs8.js b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js new file mode 100644 index 00000000000000..b4a1b586d21d10 --- /dev/null +++ b/test/parallel/test-crypto-pqc-encrypted-pkcs8.js @@ -0,0 +1,134 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const { hasOpenSSL } = require('../common/crypto'); + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); + +const assert = require('assert'); +const { + createPrivateKey, + generateKeyPairSync, + getCiphers, +} = require('crypto'); + +const algorithms = new Set([ + 'ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87', + 'ml-kem-512', 'ml-kem-768', 'ml-kem-1024', +]); +// BoringSSL does not support ML-KEM-512. +if (process.features.openssl_is_boringssl) { + algorithms.delete('ml-kem-512'); +} + +// Exercise each CBC cipher that PBES2 may use. This covers multiple +// EVP_CIPHER_key_length values (16 / 24 / 32) and, for variable-key +// ciphers like RC2, the optional PBKDF2 keyLength INTEGER branch in +// the EncryptedPrivateKeyInfo parser. +const availableCiphers = new Set(getCiphers()); +const ciphers = [ + 'aes-128-cbc', 'aes-192-cbc', 'aes-256-cbc', + 'des-ede3-cbc', 'rc2-cbc', +].filter((c) => availableCiphers.has(c)); + +const passphrase = 'top secret'; +const wrongPassphraseError = + /bad decrypt|DECRYPTION_FAILED|BAD_DECRYPT|bad password|DECODE[ _]ERROR/i; +// A wrong passphrase usually fails during cipher finalization, but CBC output +// can have valid padding by chance. OpenSSL then parses the bad plaintext as +// PKCS#8 and may report ASN.1 or decoder errors from the same failed import. +function assertWrongPassphrase(fn) { + assert.throws(fn, (err) => wrongPassphraseError.test(err.message) || + err.code?.startsWith('ERR_OSSL_ASN1_') || + err.code === 'ERR_OSSL_UNSUPPORTED'); +} + +for (const asymmetricKeyType of algorithms) { + const { privateKey } = generateKeyPairSync(asymmetricKeyType); + assert.strictEqual(privateKey.asymmetricKeyType, asymmetricKeyType); + + const plainDer = privateKey.export({ type: 'pkcs8', format: 'der' }); + + for (const cipher of ciphers) { + for (const format of ['pem', 'der']) { + const encrypted = privateKey.export({ + type: 'pkcs8', + format, + cipher, + passphrase, + }); + + const imported = createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase, + }); + assert.strictEqual(imported.type, 'private'); + assert.strictEqual(imported.asymmetricKeyType, asymmetricKeyType); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + plainDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encrypted, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } + } +} + +// Cross-implementation compatibility: load encrypted PKCS#8 fixtures that +// were generated by OpenSSL's `openssl pkcs8` from the seed-only PQC +// PrivateKeyInfo fixtures. The inner seed-only form is portable across +// OpenSSL (>=3.5) and BoringSSL, and the matching JWK fixture provides the +// canonical key material used to derive the expected PKCS#8 bytes. +const fixtures = require('../common/fixtures'); +const fixtureCases = [ + { alg: 'ml-dsa-44', jwkFile: 'ml-dsa-44.json', + encBase: 'ml_dsa_44_private_encrypted' }, + { alg: 'ml-kem-768', jwkFile: 'ml-kem-768.json', + encBase: 'ml_kem_768_private_encrypted' }, +]; + +for (const { alg, jwkFile, encBase } of fixtureCases) { + const jwkKey = createPrivateKey({ + key: JSON.parse(fixtures.readKey(jwkFile, 'utf8')), + format: 'jwk', + }); + assert.strictEqual(jwkKey.asymmetricKeyType, alg); + const expectedDer = jwkKey.export({ type: 'pkcs8', format: 'der' }); + + for (const format of ['pem', 'der']) { + const encryptedFixture = fixtures.readKey( + `${encBase}.${format}`, + format === 'pem' ? 'utf8' : null, + ); + + const imported = createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'password', + }); + assert.strictEqual(imported.asymmetricKeyType, alg); + assert.deepStrictEqual( + imported.export({ type: 'pkcs8', format: 'der' }), + expectedDer, + ); + + assertWrongPassphrase(() => createPrivateKey({ + key: encryptedFixture, + format, + type: 'pkcs8', + passphrase: 'wrong', + })); + } +} diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js index aef1012098fbb9..883c2b7123844e 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-dsa.js @@ -26,6 +26,7 @@ for (const [asymmetricKeyType, pubLen] of [ private: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private'), 'ascii'), private_seed_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_seed_only'), 'ascii'), private_priv_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_priv_only'), 'ascii'), + jwk: JSON.parse(fixtures.readKey(`${asymmetricKeyType}.json`)), }; function assertJwk(jwk) { @@ -79,10 +80,6 @@ for (const [asymmetricKeyType, pubLen] of [ key.export({ format: 'der', type: 'pkcs8' }); if (hasSeed) { assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); - } else { - assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_priv_only); - } - if (hasSeed) { const jwk = key.export({ format: 'jwk' }); assertPrivateJwk(jwk); assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); @@ -97,12 +94,13 @@ for (const [asymmetricKeyType, pubLen] of [ }); assert.strictEqual(importedPriv.equals(key), true); } else { + assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_priv_only); assert.throws(() => key.export({ format: 'jwk' }), { code: 'ERR_CRYPTO_OPERATION_FAILED', message: 'key does not have an available seed' }); } } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -117,81 +115,42 @@ for (const [asymmetricKeyType, pubLen] of [ assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], + for (const [pem, hasSeed, seedOnly] of [ + [keys.private, true, false], + [keys.private_seed_only, true, true], + [keys.private_priv_only, false, false], ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); assert.strictEqual(pubFromPriv.equals(publicKey), true); } } - } -} -{ - const format = 'jwk'; - const jwk = { - kty: 'AKP', - alg: 'ML-DSA-44', - priv: '9_uqvxH0WKJFgfLyse1a1des2bwPgsHctl_jCt5AfEo', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'SXghXj9P-DJ5eznNa_zLJxRxpa0mt86WlIid0EVEv1qraLBkC1UKevSZrjtgo1QUEN0oa3tP-HyYj8Onnc1zEnxsSoeC5A-PgywKgYuZP581wGPS-cbA2-5acsg-YUi_9fDkLR5YOTQQ3Iu952K1m8w0QDIBxZjecm32HgkD56CCC6ZyBOwfx9qcNUeO0aImya1igzL2_LRsqomogl9OuduWhtussAavGlAK7ZR4_4lmyjWcdeIc-z--iy42biV5d_tnopfNTJFlycBKinZu3h0lr4-ldl6apGDIyvSdZulhgj_j6jgEX-AgQZgS93ctx680GvROkBL7YI_3iXW3REWzVgS9HLasagEi2h6-RYQ9RzgUTODbei5fNRj3bNSqr8IKTZ08DCsRasN61TGwE3F7meoauw2NYkV51mhTxIafwhLWJrRA4C09Y-afrOtqk6a7Fiy21ObP95TGujXwThuwQSjKcUzTdCbD94ERhleZLqnPYEpb6_Jcc1OBY3kUJvCjoUhXwbW6PhWr533JDEFHoNCkPfhHS7vVCUFx4mQASkPLBud5arFSZU1uDStuiftJXfnQTWaMoJeA1N6rywB3xwLH__lHZQwEh4KnuYVuCeOMU1t8inuHI4EpZ4iTi2LrL0Cl6HadpHv-GENYwuPDVq9qg2Mo75o1X6wpPSN1J5KUDAATyR_0hurg4A1DlVpVWtykP5YWEmx_g5w4MZfEVwH-JJjEhJRxLKajxrjfG4XlnwwxPTznr1k1Mb7getKbLSbiMA3fvAgl1IjBIB8eFaISauFPpLPSpKHCVZrQYPIKSxMVdlXHgwm3CRrkR29GevCM5iSwRHQK6_HWfIQIlJ8H7uVQqXkNMvNmFldnfi3dj-oY6wMhs1ffP4RAsb5UTljvhJc6GoygBL_b0rv4aKcywDF8wa2P6B8gGFl6cVvWBQmWLJ-HL5RR68J_OmvJUm3PD-wwX3YigStd6thdTNlHOZhl4ysn8ulkFY3Rz2jEBV_nO6EXBLdOmxn_yX77qQ9yPcE64uC8iDTFWpQU1gmOF38od96oYD-T-whVl1NLD2bOvFVdd4UmWpb3Ui8AYzKzFBHNczAogQfplFmr8VABsgtWk8hW8csam70NADWK54SZOPQHeiOt1Mb488OZiDpX0FifEnCac78C_5uEiOPa8FAHpgUJ_XeXg83doDsAvrE1ZkrgFDwzT5pUTLyqh9eI1PAQKCoKmAbofcZM65o_qGvmnpN8fGVudOoHb0_Dqu_E2RBbaLcxsIe5jWmGmth4sb_4ANLFCmtt8T8sDmOdcYtQmhpUzg6rjDqeU76yq6fC15bGjT6Qc-EAgrUftFVwLw2UIGAbHmeAyFbSuJiMaUDJeYoZ3zxoID_DRCP9kN3ty-EQMHM9BZgXlH9dJ8ZUCAeH59h4PinM5LQuiS_kvP1iyT1Be6sYglV2dB7W_AziOcrrBiLfjazbUUpwqLm4_Yt_QwYJrAWfYyFOxkKxkT5qi2c1RNGBtiYjv8X_TRJDg0uxX_Hbq0Pc7yYezFQdFYIlRAvJcnc8PiOfhWtQZpCIYKTskg_Y2UfVvjydXYcGuIA2700PzR9ga-1VR7mv9UOLHe2x4ALiOO7Iz6KgOfVYMJ9dIC7f4HY9nrnLdhfKw5dcIf7RDhDqrkPyz8LLTuAuO-hGSwoP35XFkf0FQ8f1Cg5J_k-S3S2dCj8DXIRLcEJ9Qb5zvDofIPSmNKlvwJkqplDLBWlAow' - }; + // JWK import error tests + const format = 'jwk'; + const jwk = keys.jwk; - if (hasOpenSSL(3, 5)) { - assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: 'ml-dsa-44' } }), - { code: 'ERR_INVALID_ARG_VALUE', message: /must be one of: 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'/ }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: asymmetricKeyType } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: undefined } }), - { code: 'ERR_INVALID_ARG_VALUE', message: /must be one of: 'ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87'/ }); + { code: 'ERR_CRYPTO_INVALID_JWK' }); assert.throws(() => createPrivateKey({ format, key: { ...jwk, pub: undefined } }), - { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.pub" property must be of type string/ }); + { code: 'ERR_CRYPTO_INVALID_JWK' }); assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: undefined } }), - { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.priv" property must be of type string/ }); + { code: 'ERR_CRYPTO_INVALID_JWK', message: /JWK does not contain private key material/ }); assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: Buffer.alloc(33).toString('base64url') } }), { code: 'ERR_CRYPTO_INVALID_JWK' }); - assert.throws(() => createPublicKey({ format, key: { ...jwk, pub: Buffer.alloc(1313).toString('base64url') } }), + // eslint-disable-next-line @stylistic/js/max-len + assert.throws(() => createPublicKey({ format, key: { kty: jwk.kty, alg: jwk.alg, pub: Buffer.alloc(pubLen + 1).toString('base64url') } }), { code: 'ERR_CRYPTO_INVALID_JWK' }); - assert.ok(createPrivateKey({ format, key: jwk })); - assert.ok(createPublicKey({ format, key: jwk })); - - // Test vectors from ietf-cose-dilithium - { - for (const jwk of [ - { - kty: 'AKP', - alg: 'ML-DSA-44', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'unH59k4RuutY-pxvu24U5h8YZD2rSVtHU5qRZsoBmBMcRPgmu9VuNOVdteXi1zNIXjnqJg_GAAxepLqA00Vc3lO0bzRIKu39VFD8Lhuk8l0V-cFEJC-zm7UihxiQMMUEmOFxe3x1ixkKZ0jqmqP3rKryx8tSbtcXyfea64QhT6XNje2SoMP6FViBDxLHBQo2dwjRls0k5a-XSQSu2OTOiHLoaWsLe8pQ5FLNfTDqmkrawDEdZyxr3oSWJAsHQxRjcIiVzZuvwxYy1zl2STiP2vy_fTBaPemkleynQzqPg7oPCyXEE8bjnJbrfWkbNNN8438e6tHPIX4l7zTuzz98YPhLjt_d6EBdT4MldsYe-Y4KLyjaGHcAlTkk9oa5RhRwW89T0z_t1DSO3dvfKLUGXh8gd1BD6Fz5MfgpF5NjoafnQEqDjsAAhrCXY4b-Y3yYJEdX4_dp3dRGdHG_rWcPmgX4JG7lCnser4f8QGnDriqiAzJYEXeS8LzUngg_0bx0lqv_KcyU5IaLISFO0xZSU5mmEPvdSoDnyAcV8pV44qhLtAvd29n0ehG259oRihtljTWeiu9V60a1N2tbZVl5mEqSK-6_xZvNYA1TCdzNctvweH24unV7U3wer9XA9Q6kvJWDVJ4oKaQsKMrCSMlteBJMRxWbGK7ddUq6F7GdQw-3j2M-qdJvVKm9UPjY9rc1lPgol25-oJxTu7nxGlbJUH-4m5pevAN6NyZ6lfhbjWTKlxkrEKZvQXs_Yf6cpXEwpI_ZJeriq1UC1XHIpRkDwdOY9MH3an4RdDl2r9vGl_IwlKPNdh_5aF3jLgn7PCit1FNJAwC8fIncAXgAlgcXIpRXdfJk4bBiO89GGccSyDh2EgXYdpG3XvNgGWy7npuSoNTE7WIyblAk13UQuO4sdCbMIuriCdyfE73mvwj15xgb07RZRQtFGlFTmnFcIdZ90zDrWXDbANntv7KCKwNvoTuv64bY3HiGbj-NQ-U9eMylWVpvr4hrXcES8c9K3PqHWADZC0iIOvlzFv4VBoc_wVflcOrL_SIoaNFCNBAZZq-2v5lAgpJTqVOtqJ_HVraoSfcKy5g45p-qULunXj6Jwq21fobQiKubBKKOZwcJFyJD7F4ACKXOrz-HIvSHMCWW_9dVrRuCpJw0s0aVFbRqopDNhu446nqb4_EDYQM1tTHMozPd_jKxRRD0sH75X8ZoToxFSpLBDbtdWcenxj-zBf6IGWfZnmaetjKEBYJWC7QDQx1A91pJVJCEgieCkoIfTqkeQuePpIyu48g2FG3P1zjRF-kumhUTfSjo5qS0YiZQy0E1BMs6M11EvuxXRsHClLHoy5nLYI2Sj4zjVjYyxSHyPRPGGo9hwB34yWxzYNtPPGiqXS_dNCpi_zRZwRY4lCGrQ-hYTEWIK1Dm5OlttvC4_eiQ1dv63NiGkLRJ5kJA3bICN0fzCDY-MBqnd1cWn8YVBijVkgtaoascjL9EywDgJdeHnXK0eeOvUxHHhXJVkNqcibn8O4RQdpVU60TSA-uiu675ytIjcBHC6kTv8A8pmkj_4oypPd-F92YIJC741swkYQoeIHj8rE-ThcMUkF7KqC5VORbZTRp8HsZSqgiJcIPaouuxd1-8Rxrid3fXkE6p8bkrysPYoxWEJgh7ZFsRCPDWX-yTeJwFN0PKFP1j0F6YtlLfK5wv-c4F8ZQHA_-yc_gODicy7KmWDZgbTP07e7gEWzw4MFRrndjbDQ', - priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - }, - { - kty: 'AKP', - alg: 'ML-DSA-65', - // eslint-disable-next-line @stylistic/js/max-len - pub: 'QksvJn5Y1bO0TXGs_Gpla7JpUNV8YdsciAvPof6rRD8JQquL2619cIq7w1YHj22ZolInH-YsdAkeuUr7m5JkxQqIjg3-2AzV-yy9NmfmDVOevkSTAhnNT67RXbs0VaJkgCufSbzkLudVD-_91GQqVa3mk4aKRgy-wD9PyZpOMLzP-opHXlOVOWZ067galJN1h4gPbb0nvxxPWp7kPN2LDlOzt_tJxzrfvC1PjFQwNSDCm_l-Ju5X2zQtlXyJOTZSLQlCtB2C7jdyoAVwrftUXBFDkisElvgmoKlwBks23fU0tfjhwc0LVWXqhGtFQx8GGBQ-zol3e7P2EXmtIClf4KbgYq5u7Lwu848qwaItyTt7EmM2IjxVth64wHlVQruy3GXnIurcaGb_qWg764qZmteoPl5uAWwuTDX292Sa071S7GfsHFxue5lydxIYvpVUu6dyfwuExEubCovYMfz_LJd5zNTKMMatdbBJg-Qd6JPuXznqc1UYC3CccEXCLTOgg_auB6EUdG0b_cy-5bkEOHm7Wi4SDipGNig_ShzUkkot5qSqPZnd2I9IqqToi_0ep2nYLBB3ny3teW21Qpccoom3aGPt5Zl7fpzhg7Q8zsJ4sQ2SuHRCzgQ1uxYlFx21VUtHAjnFDSoMOkGyo4gH2wcLR7-z59EPPNl51pljyNefgCnMSkjrBPyz1wiET-uqi23f8Bq2TVk1jmUFxOwdfLsU7SIS30WOzvwD_gMDexUFpMlEQyL1-Y36kaTLjEWGCi2tx1FTULttQx5JpryPW6lW5oKw5RMyGpfRliYCiRyQePYqipZGoxOHpvCWhCZIN4meDY7H0RxWWQEpiyCzRQgWkOtMViwao6Jb7wZWbLNMebwLJeQJXWunk-gTEeQaMykVJobwDUiX-E_E7fSybVRTZXherY1jrvZKh8C5Gi5VADg5Vs319uN8-dVILRyOOlvjjxclmsRcn6HEvTvxd9MS7lKm2gI8BXIqhzgnTdqNGwTpmDHPV8hygqJWxWXCltBSSgY6OkGkioMAmXjZjYq_Ya9o6AE7WU_hUdm-wZmQLExwtJWEIBdDxrUxA9L9JL3weNyQtaGItPjXcheZiNBBbJTUxXwIYLnXtT1M0mHzMqGFFWXVKsN_AIdHyv4yDzY9m-tuQRfbQ_2K7r5eDOL1Tj8DZ-s8yXG74MMBqOUvlglJNgNcbuPKLRPbSDoN0E3BYkfeDgiUrXy34a5-vU-PkAWCsgAh539wJUUBxqw90V1Du7eTHFKDJEMSFYwusbPhEX4ZTwoeTHg--8Ysn4HCFWLQ00pfBCteqvMvMflcWwVfTnogcPsJb1bEFVSc3nTzhk6Ln8J-MplyS0Y5mGBEtVko_WlyeFsoDCWj4hqrgU7L-ww8vsCRSQfskH8lodiLzj0xmugiKjWUXbYq98x1zSnB9dmPy5P3UNwwMQdpebtR38N9I-jup4Bzok0-JsaOe7EORZ8ld7kAgDWa4K7BAxjc2eD540Apwxs-VLGFVkXbQgYYeDNG2tW1Xt20-XezJqZVUl6-IZXsqc7DijwNInO3fT5o8ZAcLKUUlzSlEXe8sIlHaxjLoJ-oubRtlKKUbzWOHeyxmYZSxYqQhSQj4sheedGXJEYWJ-Y5DRqB-xpy-cftxL10fdXIUhe1hWFBAoQU3b5xRY8KCytYnfLhsFF4O49xhnax3vuumLpJbCqTXpLureoKg5PvWfnpFPB0P-ZWQN35mBzqbb3ZV6U0rU55DvyXTuiZOK2Z1TxbaAd1OZMmg0cpuzewgueV-Nh_UubIqNto5RXCd7vqgqdXDUKAiWyYegYIkD4wbGMqIjxV8Oo2ggOcSj9UQPS1rD5u0rLckAzsxyty9Q5JsmKa0w8Eh7Jwe4Yob4xPVWWbJfm916avRgzDxXo5gmY7txdGFYHhlolJKdhBU9h6f0gtKEtbiUzhp4IWsqAR8riHQs7lLVEz6P537a4kL1r5FjfDf_yjJDBQmy_kdWMDqaNln-MlKK8eENjUO-qZGy0Ql4bMZtNbHXjfJUuSzapA-RqYfkqSLKgQUOW8NTDKhUk73yqCU3TQqDEKaGAoTsPscyMm7u_8QrvUK8kbc-XnxrWZ0BZJBjdinzh2w-QvjbWQ5mqFp4OMgY94__tIU8vvCUNJiYA1RdyodlfPfH5-avpxOCvBD6C7ZIDyQ-6huGEQEAb6DP8ydWIZQ8xY603DoEKKXkJWcP6CJo3nHFEdj_vcEbDQ-WESDpcQFa1fRIiGuALj-sEWcjGdSHyE8QATOcuWl4TLVzRPKAf4tCXx1zyvhJbXQu0jf0yfzVpOhPun4n-xqK4SxPBCeuJOkQ2VG9jDXWH4pnjbAcrqjveJqVti7huMXTLGuqU2uoihBw6mGqu_WSlOP2-XTEyRyvxbv2t-z9V6GPt1V9ceBukA0oGwtJqgD-q7NXFK8zhw7desI5PZMXf3nuVgbJ3xdvAlzkmm5f9RoqQS6_hqwPQEcclq1MEZ3yML5hc99TDtZWy9gGkhR0Hs3QJxxgP7bEqGFP-HjTPnJsrGaT6TjKP7qCxJlcFKLUr5AU_kxMULeUysWWtSGJ9mpxBvsyW1Juo', - priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - }, - { - kty: 'AKP', - alg: 'ML-DSA-87', - // eslint-disable-next-line @stylistic/js/max-len - pub: '5F_8jMc9uIXcZi5ioYzY44AylxF_pWWIFKmFtf8dt7Roz8gruSnx2Gt37RT1rhamU2h3LOUZEkEBBeBFaXWukf22Q7US8STV5gvWi4x-Mf4Bx7DcZa5HBQHMVlpuHfz8_RJWVDPEr-3VEYIeLpYQxFJ14oNt7jXO1p1--mcv0eQxi-9etuiX6LRRqiAt7QQrKq73envj9pkUbaIpqL2z_6SWRFln51IXv7yQSPmVZEPYcx-DPrMN4Q2slv_-fPZeoERcPjHoYB4TO-ahAHZP4xluJncmRB8xdR-_mm9YgGRPTnJ15X3isPEF5NsFXVDdHJyTT931NbjeKLDHTARJ8iLNLtC7j7x3XM7oyUBmW0D3EvT34AdQ6eHkzZz_JdGUXD6bylPM1PEu7nWBhW69aPJoRZVuPnvrdh8P51vdMb_i-gGBEzl7OHvVnWKmi4r3-iRauTLmn3eOLO79ITBPu4CZ6hPY6lfBgTGXovda4lEHW1Ha04-FNmnp1fmKNlUJiUGZOhWUhg-6cf5TDuXCn1jyl4r2iMy3Wlg4o1nBEumOJahYOsjawfhh_Vjir7pd5aUuAgkE9bQrwIdONb788-YRloR2jzbgCPBHEhd86-YnYHOB5W6q7hYcFym43lHb3kdNSMxoJJ6icWK4eZPmDITtbMZCPLNnbZ61CyyrWjoEnvExOB1iP6b7y8nbHnzAJeoEGLna0sxszU6V-izsJP7spwMYp1Fxa3IT9j7b9lpjM4NX-Dj5TsBxgiwkhRJIiFEHs9HE6SRnjHYU6hrwOBBGGfKuNylAvs-mninLtf9sPiCke-Sk90usNMEzwApqcGrMxv_T2OT71pqZcE4Sg8hQ2MWNHldTzZWHuDxMNGy5pYE3IT7BCDTGat_iu1xQGo7y7K3Rtnej3xpt64br8HIsT1Aw4g-QGN1bb8U-6iT9kre1tAJf6umW0-SP1MZQ2C261-r5NmOWmFEvJiU9LvaEfIUY6FZcyaVJXG__V83nMjiCxUp9tHCrLa-P_Sv3lPp8aS2ef71TLuzB14gOLKCzIWEovii0qfHRUfrJeAiwvZi3tDphKprIZYEr_qxvR0YCd4QLUqOwh_kWynztwPdo6ivRnqIRVfhLSgTEAArSrgWHFU1WC8Ckd6T5MpqJhN0x6x8qBePZGHAdYwz8qa9h7wiNLFWBrLRj5DmQLl1CVxnpVrjW33MFso4P8n060N4ghdKSSZsZozkNQ5b7O6yajYy-rSp6QpD8msb8oEX5imFKRaOcviQ2D4TRT45HJxKs63Tb9FtT1JoORzfkdv_E1bL3zSR6oYbTt2Stnpz-7kVqc8KR2N45EkFKxDkRw3IXOte0cq81xoU87S_ntf4KiVZaszuqb2XN2SgxnXBl4EDnpehPmqkD92SAlLrQcTaxaSe47G28K-8MwoVt4eeVkj4UEsSfJN7rbCH2yKl2XJx5huDaS0xn2ODQyNRmgk-5I9hXMUiZDNLvEzx4zuyrcu2d0oXFo3ZoUtVFNCB__TQCf2x27ej9GjLXLDAEi7qnl9Xfb94n0IfeVyGte3-j6NP3DWv8OrLiUjNTaLv6Fay1yzfUaU6LI86-Jd6ckloiGhg7kE0_hd-ZKakZxU1vh0Vzc6DW7MFAPky75iCZlDXoBpZjTNGo5HR-mCW_ozblu60U9zZA8bn-voANuu_hYwxh-uY1sHTFZOqp2xicnnMChz_GTm1Je8XCkICYegeiHUryEHA6T6B_L9gW8S_R4ptMD0Sv6b1KHqqKeubwKltCWPUsr2En9iYypnz06DEL5Wp8KMhrLid2AMPpLI0j1CWGJExXHpBWjfIC8vbYH4YKVl-euRo8eDcuKosb5hxUGM9Jvy1siVXUpIKpkZt2YLP5pEBP_EVOoHPh5LJomrLMpORr1wBKbEkfom7npX1g817bK4IeYmZELI8zXUUtUkx3LgNTckwjx90Vt6oVXpFEICIUDF_LAVMUftzz6JUvbwOZo8iAZqcnVslAmRXeY_ZPp5eEHFfHlsb8VQ73Rd_p8XlFf5R1WuWiUGp2TzJ-VQvj3BTdQfOwSxR9RUk4xjqNabLqTFcQ7As246bHJXH6XVnd4DbEIDPfNa8FaWb_DNEgQAiXGqa6n7l7aFq5_6Kp0XeBBM0sOzJt4fy8JC6U0DEcMnWxKFDtMM7q06LubQYFCEEdQ5b1Qh2LbQZ898tegmeF--EZ4F4hvYebZPV8sM0ZcsKBXyCr585qs00PRxr0S6rReekGRBIvXzMojmid3dxc6DPpdV3x5zxlxaIBxO3i_6axknSSdxnS04_bemWqQ3CLf6mpSqfTIQJT1407GB4QINAAC9Ch3AXUR_n1jr64TGWzbIr8uDcnoVCJlOgmlXpmOwubigAzJattbWRi7k4QYBnA3_4QMjt73n2Co4-F_Qh4boYLpmwWG2SwcIw2PeXGr2LY2zwkPR4bcSyx1Z6UK5trQpWlpQCxgsvV_RvGzpN22RtHoihPH74K0cBIzCz7tK-jqeuWl1A7af7KmQ66fpRBr5ykTLOsa17WblkcIB_jDvqKfEcdxhPWJUwmOo4TIQS-xH8arLOy_NQFG2m14_yxwUemXC-QxLUYi6_FIcqwPBKjCdpQtadRdyftQSKO0SP-GxUvamMZzWI780rXuOBkq5kyYLy9QF9bf_-bL6QLpe1WMCQlOeXZaCPoncgYoT0WZ17jB52Xb2lPWsyXYK54npszkbKJ4OIqfvF8xqRXcVe22VwJuqT9Uy4-4KKQgQ7TXla7Gdm2H7mKl8YXQlsGCT2Ypc8O4t0Sfw7qYAuaDGf752Hbm3fl1bupcB2huIPlIaDP6IRR9XvTYIW2flbwYfhKLmoVKnG85uUi2qtqCjPOIuU3-peT0othfmwKQXaoOqO-V4r6wPL1VHxVFtIYmEdVt0RccUOvpOVR_OAHG9uHOzTmueK5557Qxp0ojtZCHyN-hgoMZJLrvdKkTCxPNo2-mZQbHoVh2FnThZ9JbO49dB8lKXP4_MU5xAnjXMgKXtbfI8w6ZWATE_XWgf2VQMUpGp4wpy44yWQTxHxh_4T9540BGwG0FU0bkgrwA_erseGZnepqdmz5_ScCs84O5Xr5MbYhJLCGGxY6O5GqS-ooB2w0Mt87KbbE4bpYje9CAHH8FX3pDrJyLsyasA3zxmk4OmGpG7Z70ofONJtHRe56R5287vFmuazEEutXn81kNzB-3aJT1ga3vnWZw4CSvFKoWYSA7auLgrHSHFZdITfOrgtmQmGbFhM9kSBdY1UCnpzf65oos3PZWRa2twfUxxLAnPNtrxpRGyvtsapw7ljUagZmuyh3hLCjhAxYmnoE1dbyIWvpCqSlEtVjL1yb_nuLEzgvmZuV02fHxGuWgHTOMVGXpf81Rce3eoBK3lapW1wkzezlk3tcA2bZOtA9qbxdsbVR37kemzQ9K1e3Y0OWhtSj', - priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' - }, - ]) { - assert.partialDeepStrictEqual(jwk, createPublicKey({ format, key: jwk }).export({ format: 'jwk' })); - assert.deepStrictEqual(createPrivateKey({ format, key: jwk }).export({ format: 'jwk' }), jwk); - } - - } - } else { - assert.throws(() => createPrivateKey({ format, key: jwk }), - { code: 'ERR_INVALID_ARG_VALUE', message: /Unsupported key type/ }); - assert.throws(() => createPublicKey({ format, key: jwk }), - { code: 'ERR_INVALID_ARG_VALUE', message: /Unsupported key type/ }); + // JWK round-trip + assert.partialDeepStrictEqual(jwk, createPublicKey({ format, key: jwk }).export({ format })); + assert.deepStrictEqual(createPrivateKey({ format, key: jwk }).export({ format }), jwk); } } diff --git a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js index 5990258b04f697..de0755ca652cab 100644 --- a/test/parallel/test-crypto-pqc-key-objects-ml-kem.js +++ b/test/parallel/test-crypto-pqc-key-objects-ml-kem.js @@ -18,14 +18,35 @@ function getKeyFileName(type, suffix) { return `${type.replaceAll('-', '_')}_${suffix}.pem`; } -for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { +for (const [asymmetricKeyType, pubLen] of [ + ['ml-kem-512', 800], ['ml-kem-768', 1184], ['ml-kem-1024', 1568], +]) { const keys = { public: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'public'), 'ascii'), private: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private'), 'ascii'), private_seed_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_seed_only'), 'ascii'), private_priv_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_priv_only'), 'ascii'), + jwk: JSON.parse(fixtures.readKey(`${asymmetricKeyType}.json`)), }; + function assertJwk(jwk) { + assert.strictEqual(jwk.kty, 'AKP'); + assert.strictEqual(jwk.alg, asymmetricKeyType.toUpperCase()); + assert.ok(jwk.pub); + assert.strictEqual(Buffer.from(jwk.pub, 'base64url').byteLength, pubLen); + } + + function assertPublicJwk(jwk) { + assertJwk(jwk); + assert.ok(!jwk.priv); + } + + function assertPrivateJwk(jwk) { + assertJwk(jwk); + assert.ok(jwk.priv); + assert.strictEqual(Buffer.from(jwk.priv, 'base64url').byteLength, 64); + } + function assertKey(key) { assert.deepStrictEqual(key.asymmetricKeyDetails, {}); assert.strictEqual(key.asymmetricKeyType, asymmetricKeyType); @@ -38,12 +59,14 @@ for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { assert.strictEqual(key.type, 'public'); assert.strictEqual(key.export({ format: 'pem', type: 'spki' }), keys.public); key.export({ format: 'der', type: 'spki' }); - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + const jwk = key.export({ format: 'jwk' }); + assertPublicJwk(jwk); + assert.strictEqual(key.equals(createPublicKey({ format: 'jwk', key: jwk })), true); // Raw format round-trip const rawPub = key.export({ format: 'raw-public' }); assert(Buffer.isBuffer(rawPub)); + assert.strictEqual(rawPub.byteLength, pubLen); const importedPub = createPublicKey({ key: rawPub, format: 'raw-public', asymmetricKeyType, }); @@ -57,22 +80,27 @@ for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { key.export({ format: 'der', type: 'pkcs8' }); if (hasSeed) { assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); + const jwk = key.export({ format: 'jwk' }); + assertPrivateJwk(jwk); + assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); + assert.ok(createPublicKey({ format: 'jwk', key: jwk })); // Raw seed round-trip const rawSeed = key.export({ format: 'raw-seed' }); assert(Buffer.isBuffer(rawSeed)); + assert.strictEqual(rawSeed.byteLength, 64); const importedPriv = createPrivateKey({ key: rawSeed, format: 'raw-seed', asymmetricKeyType, }); assert.strictEqual(importedPriv.equals(key), true); } else { assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private_priv_only); + assert.throws(() => key.export({ format: 'jwk' }), + { code: 'ERR_CRYPTO_OPERATION_FAILED', message: 'key does not have an available seed' }); } - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); } - if (!hasOpenSSL(3, 5)) { + if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { assert.throws(() => createPublicKey(keys.public), { code: hasOpenSSL(3) ? 'ERR_OSSL_EVP_DECODE_ERROR' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); @@ -82,21 +110,55 @@ for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { code: hasOpenSSL(3) ? 'ERR_OSSL_UNSUPPORTED' : 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM', }); } + } else if (process.features.openssl_is_boringssl && asymmetricKeyType === 'ml-kem-512') { + // BoringSSL does not support ML-KEM-512. + assert.throws(() => createPublicKey(keys.public), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + for (const pem of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + assert.throws(() => createPrivateKey(pem), + { code: 'ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM' }); + } } else { const publicKey = createPublicKey(keys.public); assertPublicKey(publicKey); { - for (const [pem, hasSeed] of [ - [keys.private, true], - [keys.private_seed_only, true], - [keys.private_priv_only, false], - ]) { + const entries = process.features.openssl_is_boringssl ? + // BoringSSL only supports the seed-only PKCS#8 private key encoding. + [[keys.private_seed_only, true]] : + [ + [keys.private, true], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]; + for (const [pem, hasSeed] of entries) { const pubFromPriv = createPublicKey(pem); assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(pem), hasSeed); assert.strictEqual(pubFromPriv.equals(publicKey), true); } } + + // JWK import error tests + const format = 'jwk'; + const jwk = keys.jwk; + + assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: asymmetricKeyType } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, pub: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK', message: /JWK does not contain private key material/ }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: Buffer.alloc(65).toString('base64url') } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + // eslint-disable-next-line @stylistic/js/max-len + assert.throws(() => createPublicKey({ format, key: { kty: jwk.kty, alg: jwk.alg, pub: Buffer.alloc(pubLen + 1).toString('base64url') } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + + // JWK round-trip + assert.partialDeepStrictEqual(jwk, createPublicKey({ format, key: jwk }).export({ format })); + assert.deepStrictEqual(createPrivateKey({ format, key: jwk }).export({ format }), jwk); } } diff --git a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js index fdae27f2da797f..4cc8646cd8b90e 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -26,8 +26,28 @@ for (const asymmetricKeyType of [ const keys = { public: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'public'), 'ascii'), private: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private'), 'ascii'), + jwk: JSON.parse(fixtures.readKey(`${asymmetricKeyType}.json`)), }; + function assertJwk(jwk) { + assert.strictEqual(jwk.kty, 'AKP'); + // SLH-DSA algorithm names keep the last character (f/s) lowercase. + const expectedAlg = asymmetricKeyType.slice(0, -1).toUpperCase() + + asymmetricKeyType.slice(-1); + assert.strictEqual(jwk.alg, expectedAlg); + assert.ok(jwk.pub); + } + + function assertPublicJwk(jwk) { + assertJwk(jwk); + assert.ok(!jwk.priv); + } + + function assertPrivateJwk(jwk) { + assertJwk(jwk); + assert.ok(jwk.priv); + } + function assertKey(key) { assert.deepStrictEqual(key.asymmetricKeyDetails, {}); assert.strictEqual(key.asymmetricKeyType, asymmetricKeyType); @@ -40,8 +60,9 @@ for (const asymmetricKeyType of [ assert.strictEqual(key.type, 'public'); assert.strictEqual(key.export({ format: 'pem', type: 'spki' }), keys.public); key.export({ format: 'der', type: 'spki' }); - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + const jwk = key.export({ format: 'jwk' }); + assertPublicJwk(jwk); + assert.strictEqual(key.equals(createPublicKey({ format: 'jwk', key: jwk })), true); // Raw format round-trip const rawPub = key.export({ format: 'raw-public' }); @@ -58,8 +79,10 @@ for (const asymmetricKeyType of [ assertPublicKey(createPublicKey(key)); key.export({ format: 'der', type: 'pkcs8' }); assert.strictEqual(key.export({ format: 'pem', type: 'pkcs8' }), keys.private); - assert.throws(() => key.export({ format: 'jwk' }), - { code: 'ERR_CRYPTO_JWK_UNSUPPORTED_KEY_TYPE', message: 'Unsupported JWK Key Type.' }); + const jwk = key.export({ format: 'jwk' }); + assertPrivateJwk(jwk); + assert.strictEqual(key.equals(createPrivateKey({ format: 'jwk', key: jwk })), true); + assert.ok(createPublicKey({ format: 'jwk', key: jwk })); // Raw format round-trip const rawPriv = key.export({ format: 'raw-private' }); @@ -86,5 +109,27 @@ for (const asymmetricKeyType of [ assertPublicKey(pubFromPriv); assertPrivateKey(createPrivateKey(keys.private)); assert.strictEqual(pubFromPriv.equals(publicKey), true); + + // JWK import error tests + const format = 'jwk'; + const jwk = keys.jwk; + + assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: asymmetricKeyType } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, alg: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, pub: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: undefined } }), + { code: 'ERR_CRYPTO_INVALID_JWK', message: /JWK does not contain private key material/ }); + assert.throws(() => createPrivateKey({ format, key: { ...jwk, priv: Buffer.alloc(1).toString('base64url') } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + // eslint-disable-next-line @stylistic/js/max-len + assert.throws(() => createPublicKey({ format, key: { kty: jwk.kty, alg: jwk.alg, pub: Buffer.alloc(1).toString('base64url') } }), + { code: 'ERR_CRYPTO_INVALID_JWK' }); + + // JWK round-trip + assert.partialDeepStrictEqual(jwk, createPublicKey({ format, key: jwk }).export({ format })); + assert.deepStrictEqual(createPrivateKey({ format, key: jwk }).export({ format }), jwk); } } diff --git a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js index abad2c15cf01d1..e6534c988c4e2b 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-dsa.js @@ -11,7 +11,7 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', diff --git a/test/parallel/test-crypto-pqc-keygen-ml-kem.js b/test/parallel/test-crypto-pqc-keygen-ml-kem.js index adb5fcb56ce6ef..620f65c3a8d156 100644 --- a/test/parallel/test-crypto-pqc-keygen-ml-kem.js +++ b/test/parallel/test-crypto-pqc-keygen-ml-kem.js @@ -11,7 +11,12 @@ const { generateKeyPair, } = require('crypto'); -if (!hasOpenSSL(3, 5)) { +const algorithms = process.features.openssl_is_boringssl ? + // BoringSSL does not support ML-KEM-512. + ['ml-kem-768', 'ml-kem-1024'] : + ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']; + +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { assert.throws(() => generateKeyPair(asymmetricKeyType, common.mustNotCall()), { code: 'ERR_INVALID_ARG_VALUE', @@ -19,7 +24,23 @@ if (!hasOpenSSL(3, 5)) { }); } } else { - for (const asymmetricKeyType of ['ml-kem-512', 'ml-kem-768', 'ml-kem-1024']) { + for (const asymmetricKeyType of algorithms) { + function assertJwk(jwk) { + assert.strictEqual(jwk.kty, 'AKP'); + assert.strictEqual(jwk.alg, asymmetricKeyType.toUpperCase()); + assert.ok(jwk.pub); + } + + function assertPublicJwk(jwk) { + assertJwk(jwk); + assert.ok(!jwk.priv); + } + + function assertPrivateJwk(jwk) { + assertJwk(jwk); + assert.ok(jwk.priv); + } + for (const [publicKeyEncoding, validate] of [ /* eslint-disable node-core/must-call-assert */ [undefined, (publicKey) => { @@ -27,6 +48,7 @@ if (!hasOpenSSL(3, 5)) { assert.strictEqual(publicKey.asymmetricKeyType, asymmetricKeyType); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); }], + [{ format: 'jwk' }, (publicKey) => assertPublicJwk(publicKey)], [{ format: 'pem', type: 'spki' }, (publicKey) => assert.strictEqual(typeof publicKey, 'string')], [{ format: 'der', type: 'spki' }, (publicKey) => assert.strictEqual(Buffer.isBuffer(publicKey), true)], /* eslint-enable node-core/must-call-assert */ @@ -40,6 +62,7 @@ if (!hasOpenSSL(3, 5)) { assert.strictEqual(privateKey.asymmetricKeyType, asymmetricKeyType); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, {}); }], + [{ format: 'jwk' }, (_, privateKey) => assertPrivateJwk(privateKey)], [{ format: 'pem', type: 'pkcs8' }, (_, privateKey) => assert.strictEqual(typeof privateKey, 'string')], [{ format: 'der', type: 'pkcs8' }, (_, privateKey) => assert.strictEqual(Buffer.isBuffer(privateKey), true)], /* eslint-enable node-core/must-call-assert */ @@ -48,3 +71,10 @@ if (!hasOpenSSL(3, 5)) { } } } + +if (process.features.openssl_is_boringssl) { + assert.throws(() => generateKeyPair('ml-kem-512', common.mustNotCall()), { + code: 'ERR_INVALID_ARG_VALUE', + message: /The argument 'type' must be a supported key type/ + }); +} diff --git a/test/parallel/test-crypto-pqc-keygen-slh-dsa.js b/test/parallel/test-crypto-pqc-keygen-slh-dsa.js index 6b741e37f0cc11..a8e480818cd134 100644 --- a/test/parallel/test-crypto-pqc-keygen-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-keygen-slh-dsa.js @@ -28,6 +28,26 @@ if (!hasOpenSSL(3, 5)) { 'slh-dsa-sha2-256f', 'slh-dsa-sha2-256s', 'slh-dsa-shake-128f', 'slh-dsa-shake-128s', 'slh-dsa-shake-192f', 'slh-dsa-shake-192s', 'slh-dsa-shake-256f', 'slh-dsa-shake-256s', ]) { + + function assertJwk(jwk) { + assert.strictEqual(jwk.kty, 'AKP'); + // SLH-DSA algorithm names keep the last character (f/s) lowercase. + const expectedAlg = asymmetricKeyType.slice(0, -1).toUpperCase() + + asymmetricKeyType.slice(-1); + assert.strictEqual(jwk.alg, expectedAlg); + assert.ok(jwk.pub); + } + + function assertPublicJwk(jwk) { + assertJwk(jwk); + assert.ok(!jwk.priv); + } + + function assertPrivateJwk(jwk) { + assertJwk(jwk); + assert.ok(jwk.priv); + } + for (const [publicKeyEncoding, validate] of [ /* eslint-disable node-core/must-call-assert */ [undefined, (publicKey) => { @@ -35,6 +55,7 @@ if (!hasOpenSSL(3, 5)) { assert.strictEqual(publicKey.asymmetricKeyType, asymmetricKeyType); assert.deepStrictEqual(publicKey.asymmetricKeyDetails, {}); }], + [{ format: 'jwk' }, (publicKey) => assertPublicJwk(publicKey)], [{ format: 'pem', type: 'spki' }, (publicKey) => assert.strictEqual(typeof publicKey, 'string')], [{ format: 'der', type: 'spki' }, (publicKey) => assert.strictEqual(Buffer.isBuffer(publicKey), true)], ]) { @@ -46,6 +67,7 @@ if (!hasOpenSSL(3, 5)) { assert.strictEqual(privateKey.asymmetricKeyType, asymmetricKeyType); assert.deepStrictEqual(privateKey.asymmetricKeyDetails, {}); }], + [{ format: 'jwk' }, (_, privateKey) => assertPrivateJwk(privateKey)], [{ format: 'pem', type: 'pkcs8' }, (_, privateKey) => assert.strictEqual(typeof privateKey, 'string')], [{ format: 'der', type: 'pkcs8' }, (_, privateKey) => assert.strictEqual(Buffer.isBuffer(privateKey), true)], /* eslint-enable node-core/must-call-assert */ diff --git a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js index 57d6692ca79b55..535e6a33d5ccb0 100644 --- a/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js +++ b/test/parallel/test-crypto-pqc-sign-verify-ml-dsa.js @@ -6,8 +6,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { @@ -34,7 +34,15 @@ for (const [asymmetricKeyType, sigLen] of [ private_priv_only: fixtures.readKey(getKeyFileName(asymmetricKeyType, 'private_priv_only'), 'ascii'), }; - for (const privateKey of [keys.private, keys.private_seed_only, keys.private_priv_only]) { + for (const [privateKey, seedOnly] of [ + [keys.private, false], + [keys.private_seed_only, true], + [keys.private_priv_only, false], + ]) { + if (process.features.openssl_is_boringssl && !seedOnly) { + common.printSkipMessage('Skipping unsupported private key format test'); + continue; + } for (const data of [randomBytes(0), randomBytes(1), randomBytes(32), randomBytes(128), randomBytes(1024)]) { // sync { @@ -44,10 +52,12 @@ for (const [asymmetricKeyType, sigLen] of [ assert.strictEqual(verify(undefined, data, keys.public, Buffer.alloc(sigLen)), false); assert.strictEqual(verify(undefined, data, keys.public, signature), true); assert.strictEqual(verify(undefined, data, privateKey, signature), true); - assert.throws(() => sign('sha256', data, privateKey), { code: 'ERR_OSSL_INVALID_DIGEST' }); + const code = process.features.openssl_is_boringssl ? + 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_OSSL_INVALID_DIGEST'; + assert.throws(() => sign('sha256', data, privateKey), { code }); assert.throws( () => verify('sha256', data, keys.public, Buffer.alloc(sigLen)), - { code: 'ERR_OSSL_INVALID_DIGEST' }); + { code }); } // async @@ -62,8 +72,9 @@ for (const [asymmetricKeyType, sigLen] of [ })); })); - sign('sha256', data, privateKey, common.expectsError(/invalid digest/)); - verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(/invalid digest/)); + const message = process.features.openssl_is_boringssl ? /COMMAND_NOT_SUPPORTED/ : /invalid digest/; + sign('sha256', data, privateKey, common.expectsError(message)); + verify('sha256', data, keys.public, Buffer.alloc(sigLen), common.expectsError(message)); } } } diff --git a/test/parallel/test-crypto-rsa-dsa.js b/test/parallel/test-crypto-rsa-dsa.js index 119bc3c2d20ea7..35ad673020778a 100644 --- a/test/parallel/test-crypto-rsa-dsa.js +++ b/test/parallel/test-crypto-rsa-dsa.js @@ -39,10 +39,19 @@ const openssl1DecryptError = { const decryptError = hasOpenSSL3 ? { message: 'error:1C800064:Provider routines::bad decrypt' } : - openssl1DecryptError; + process.features.openssl_is_boringssl ? { + message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', + code: 'ERR_OSSL_BAD_DECRYPT', + reason: 'BAD_DECRYPT', + function: 'OPENSSL_internal', + library: 'Cipher functions', + } : + openssl1DecryptError; const decryptPrivateKeyError = hasOpenSSL3 ? { message: 'error:1C800064:Provider routines::bad decrypt', +} : process.features.openssl_is_boringssl ? { + message: 'error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT', } : openssl1DecryptError; function getBufferCopy(buf) { @@ -325,9 +334,14 @@ function test_rsa(padding, encryptOaepHash, decryptOaepHash) { } test_rsa('RSA_NO_PADDING'); -test_rsa('RSA_PKCS1_PADDING'); test_rsa('RSA_PKCS1_OAEP_PADDING'); +if (!process.features.openssl_is_boringssl) { + test_rsa('RSA_PKCS1_PADDING'); +} else { + common.printSkipMessage('Skipping unsupported RSA_PKCS1_PADDING test case'); +} + // Test OAEP with different hash functions. test_rsa('RSA_PKCS1_OAEP_PADDING', undefined, 'sha1'); test_rsa('RSA_PKCS1_OAEP_PADDING', 'sha1', undefined); @@ -489,7 +503,7 @@ assert.throws(() => { // // Test DSA signing and verification // -{ +if (!process.features.openssl_is_boringssl) { const input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify @@ -512,13 +526,15 @@ assert.throws(() => { verify2.update(input); assert.strictEqual(verify2.verify(dsaPubPem, signature2, 'hex'), true); +} else { + common.printSkipMessage('Skipping unsupported DSA test case'); } // // Test DSA signing and verification with PKCS#8 private key // -{ +if (!process.features.openssl_is_boringssl) { const input = 'I AM THE WALRUS'; // DSA signatures vary across runs so there is no static string to verify @@ -531,6 +547,8 @@ assert.throws(() => { verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} else { + common.printSkipMessage('Skipping unsupported DSA test case'); } @@ -547,7 +565,7 @@ const input = 'I AM THE WALRUS'; }, decryptPrivateKeyError); } -{ +if (!process.features.openssl_is_boringssl) { // DSA signatures vary across runs so there is no static string to verify // against. const sign = crypto.createSign('SHA1'); @@ -559,4 +577,6 @@ const input = 'I AM THE WALRUS'; verify.update(input); assert.strictEqual(verify.verify(dsaPubPem, signature, 'hex'), true); +} else { + common.printSkipMessage('Skipping unsupported DSA test case'); } diff --git a/test/parallel/test-crypto-scrypt.js b/test/parallel/test-crypto-scrypt.js index eafdfe392bde8e..5effc083cda11a 100644 --- a/test/parallel/test-crypto-scrypt.js +++ b/test/parallel/test-crypto-scrypt.js @@ -192,7 +192,9 @@ for (const options of incompatibleOptions) { for (const options of toobig) { const expected = { - message: /Invalid scrypt params:.*memory limit exceeded/, + message: process.features.openssl_is_boringssl ? + /Invalid scrypt params:.*(INVALID_PARAMETERS|MEMORY_LIMIT_EXCEEDED)/ : + /Invalid scrypt params:.*memory limit exceeded/, code: 'ERR_CRYPTO_INVALID_SCRYPT_PARAMS', }; assert.throws(() => crypto.scrypt('pass', 'salt', 1, options, () => {}), diff --git a/test/parallel/test-crypto-secure-heap.js b/test/parallel/test-crypto-secure-heap.js index c20b01a91a9840..3845f49a4748b8 100644 --- a/test/parallel/test-crypto-secure-heap.js +++ b/test/parallel/test-crypto-secure-heap.js @@ -13,6 +13,10 @@ if (common.isASan) { common.skip('ASan does not play well with secure heap allocations'); } +if (process.features.openssl_is_boringssl) { + common.skip('BoringSSL does not support secure heap'); +} + const assert = require('assert'); const { fork } = require('child_process'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-crypto-sign-verify.js b/test/parallel/test-crypto-sign-verify.js index e57a1f5b596cbe..3dd5e8e8325274 100644 --- a/test/parallel/test-crypto-sign-verify.js +++ b/test/parallel/test-crypto-sign-verify.js @@ -68,7 +68,9 @@ const keySize = 2048; }); }, { message: hasOpenSSL(3) ? 'error:1C8000A5:Provider routines::illegal or unsupported padding mode' : - 'bye, bye, error stack' }); + process.features.openssl_is_boringssl ? + 'error:0600006d:public key routines:OPENSSL_internal:ILLEGAL_OR_UNSUPPORTED_PADDING_MODE' : + 'bye, bye, error stack' }); delete Object.prototype.opensslErrorStack; } @@ -347,6 +349,9 @@ assert.throws( }, hasOpenSSL(3) ? { code: 'ERR_OSSL_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', message: /illegal or unsupported padding mode/, + } : process.features.openssl_is_boringssl ? { + code: 'ERR_OSSL_EVP_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', + message: /ILLEGAL_OR_UNSUPPORTED_PADDING_MODE/, } : { code: 'ERR_OSSL_RSA_ILLEGAL_OR_UNSUPPORTED_PADDING_MODE', message: /illegal or unsupported padding mode/, @@ -418,25 +423,32 @@ assert.throws( /Invalid digest/); } -[ +for (const pair of [ { private: fixtures.readKey('ed25519_private.pem', 'ascii'), public: fixtures.readKey('ed25519_public.pem', 'ascii'), + skip: false, algo: null, supportsContext: hasOpenSSL(3, 2), sigLen: 64, raw: true }, { private: fixtures.readKey('ed448_private.pem', 'ascii'), public: fixtures.readKey('ed448_public.pem', 'ascii'), + skip: process.features.openssl_is_boringssl, algo: null, supportsContext: hasOpenSSL(3, 2), sigLen: 114, raw: true }, { private: fixtures.readKey('rsa_private_2048.pem', 'ascii'), public: fixtures.readKey('rsa_public_2048.pem', 'ascii'), + skip: false, algo: 'sha1', sigLen: 256, raw: false }, -].forEach((pair) => { +]) { + if (pair.skip) { + common.printSkipMessage('Skipping unsupported test case'); + continue; + } const algo = pair.algo; { @@ -550,7 +562,7 @@ assert.throws( }, { message: 'Context parameter is unsupported' }); } } -}); +} // Ed25519ctx: Ed25519 with context string. if (hasOpenSSL(3, 2)) { @@ -618,6 +630,10 @@ if (hasOpenSSL(3, 2)) { const keys = [['ec-key.pem', 64], ['dsa_private_1025.pem', 40]]; for (const [file, length] of keys) { + if (process.features.openssl_is_boringssl && file.startsWith('dsa_')) { + common.printSkipMessage(`Skipping unsupported ${file} test case`); + continue; + } const privKey = fixtures.readKey(file); [ crypto.createSign('sha1').update(data).sign(privKey), @@ -752,7 +768,7 @@ if (hasOpenSSL(3, 2)) { })); } -{ +if (!process.features.openssl_is_boringssl) { // Test RSA-PSS. { // This key pair does not restrict the message digest algorithm or salt @@ -861,6 +877,8 @@ if (hasOpenSSL(3, 2)) { } } } +} else { + common.printSkipMessage('Skipping unsupported RSA-PSS test cases'); } // The sign function should not swallow OpenSSL errors. @@ -873,7 +891,7 @@ if (hasOpenSSL(3, 2)) { crypto.sign('sha512', 'message', privateKey); }, { code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY', - message: /digest too big for rsa key/ + message: /digest too big for rsa key|DIGEST_TOO_BIG_FOR_RSA_KEY/ }); } @@ -891,16 +909,20 @@ if (hasOpenSSL(3, 2)) { }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); assert.throws(() => { crypto.createSign('sha256').sign({ key, format: 'jwk' }); - }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "key\.key" property must be of type object/ }); + }, { code: 'ERR_INVALID_ARG_TYPE', message: /The "privateKey\.key" property must be of type object/ }); } } { // Ed25519 and Ed448 must use the one-shot methods const keys = [{ privateKey: fixtures.readKey('ed25519_private.pem', 'ascii'), - publicKey: fixtures.readKey('ed25519_public.pem', 'ascii') }, - { privateKey: fixtures.readKey('ed448_private.pem', 'ascii'), - publicKey: fixtures.readKey('ed448_public.pem', 'ascii') }]; + publicKey: fixtures.readKey('ed25519_public.pem', 'ascii') }]; + if (!process.features.openssl_is_boringssl) { + keys.push({ privateKey: fixtures.readKey('ed448_private.pem', 'ascii'), + publicKey: fixtures.readKey('ed448_public.pem', 'ascii') }); + } else { + common.printSkipMessage('Skipping unsupported Ed448 test case'); + } for (const { publicKey, privateKey } of keys) { assert.throws(() => { @@ -915,7 +937,70 @@ if (hasOpenSSL(3, 2)) { } } +// Test that sign/verify error messages use correct property paths { + // Sign with invalid format + assert.throws(() => { + crypto.createSign('SHA256').update('test').sign({ + key: Buffer.alloc(0), format: 'banana', type: 'pkcs8', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /privateKey\.format/, + }); + + // Sign with invalid type + assert.throws(() => { + crypto.createSign('SHA256').update('test').sign({ + key: Buffer.alloc(0), format: 'der', type: 'banana', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /privateKey\.type/, + }); + + // Verify with invalid format + assert.throws(() => { + crypto.createVerify('SHA256').update('test').verify({ + key: Buffer.alloc(0), format: 'banana', type: 'spki', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // Verify with invalid type + assert.throws(() => { + crypto.createVerify('SHA256').update('test').verify({ + key: Buffer.alloc(0), format: 'der', type: 'banana', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.type/, + }); + + // crypto.sign with invalid format + assert.throws(() => { + crypto.sign('SHA256', Buffer.from('test'), { + key: Buffer.alloc(0), format: 'banana', type: 'pkcs8', + }); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); + + // crypto.verify with invalid format + assert.throws(() => { + crypto.verify('SHA256', Buffer.from('test'), { + key: Buffer.alloc(0), format: 'banana', type: 'spki', + }, Buffer.alloc(0)); + }, { + code: 'ERR_INVALID_ARG_VALUE', + message: /key\.format/, + }); +} + +if (!process.features.openssl_is_boringssl) { // Dh, x25519 and x448 should not be used for signing/verifying // https://github.com/nodejs/node/issues/53742 for (const algo of ['dh', 'x25519', 'x448']) { @@ -931,6 +1016,8 @@ if (hasOpenSSL(3, 2)) { crypto.createVerify('SHA256').update('Test123').verify(publicKey, 'sig'); }, { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE', message: /operation not supported for this keytype/ }); } +} else { + common.printSkipMessage('Skipping unsupported dh/x25519/x448 test cases'); } // crypto.verify accepts ArrayBuffer and SharedArrayBuffer for data and signature diff --git a/test/parallel/test-crypto.js b/test/parallel/test-crypto.js index d21a6bd3d98d6d..46f4571b33dfe8 100644 --- a/test/parallel/test-crypto.js +++ b/test/parallel/test-crypto.js @@ -62,7 +62,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: mac verify failure$/.test(err) && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && !('opensslErrorStack' in err); }); @@ -72,7 +72,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: mac verify failure$/.test(err) && + /^Error: (mac verify failure|INCORRECT_PASSWORD)$/.test(err) && !('opensslErrorStack' in err); }); @@ -82,7 +82,7 @@ assert.throws(() => { // Throws general Error, so there is no opensslErrorStack property. return err instanceof Error && err.name === 'Error' && - /^Error: not enough data$/.test(err) && + /^Error: (not enough data|BAD_PKCS12_DATA)$/.test(err) && !('opensslErrorStack' in err); }); @@ -145,8 +145,10 @@ assert(crypto.getHashes().includes('sha1')); assert(crypto.getHashes().includes('sha256')); assert(!crypto.getHashes().includes('SHA1')); assert(!crypto.getHashes().includes('SHA256')); -assert(crypto.getHashes().includes('RSA-SHA1')); -assert(!crypto.getHashes().includes('rsa-sha1')); +if (!process.features.openssl_is_boringssl) { + assert(crypto.getHashes().includes('RSA-SHA1')); + assert(!crypto.getHashes().includes('rsa-sha1')); +} validateList(crypto.getHashes()); // Make sure all of the hashes are supported by OpenSSL for (const algo of crypto.getHashes()) @@ -209,49 +211,72 @@ assert.throws(() => { ].join('\n'); crypto.createSign('SHA256').update('test').sign(priv); }, (err) => { - if (!hasOpenSSL3) - assert.ok(!('opensslErrorStack' in err)); - assert.throws(() => { throw err; }, hasOpenSSL3 ? { - name: 'Error', - message: 'error:02000070:rsa routines::digest too big for rsa key', - library: 'rsa routines', - } : { - name: 'Error', - message: /routines:RSA_sign:digest too big for rsa key$/, - library: /rsa routines/i, - function: 'RSA_sign', - reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, - code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' - }); + if (process.features.openssl_is_boringssl) { + // BoringSSL rejects the tiny RSA key while decoding it, before signing. + assert.throws(() => { throw err; }, { + name: 'Error', + message: 'error:06000066:public key routines:OPENSSL_internal:' + + 'DECODE_ERROR', + library: 'public key routines', + function: 'OPENSSL_internal', + reason: 'DECODE_ERROR', + code: 'ERR_OSSL_EVP_DECODE_ERROR' + }); + assert(Array.isArray(err.opensslErrorStack)); + assert(err.opensslErrorStack.length > 0); + } else { + if (!hasOpenSSL3) + assert.ok(!('opensslErrorStack' in err)); + assert.throws(() => { throw err; }, hasOpenSSL3 ? { + name: 'Error', + message: 'error:02000070:rsa routines::digest too big for rsa key', + library: 'rsa routines', + } : { + name: 'Error', + message: /routines:RSA_sign:digest too big for rsa key$/, + library: /rsa routines/i, + function: 'RSA_sign', + reason: /digest[\s_]too[\s_]big[\s_]for[\s_]rsa[\s_]key/i, + code: 'ERR_OSSL_RSA_DIGEST_TOO_BIG_FOR_RSA_KEY' + }); + } return true; }); if (!hasOpenSSL3) { - assert.throws(() => { - // The correct header inside `rsa_private_pkcs8_bad.pem` should have been - // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- - // instead of - // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- - const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', - 'ascii'); - // This would inject errors onto OpenSSL's error stack - crypto.createSign('sha1').sign(sha1_privateKey); - }, (err) => { - // Do the standard checks, but then do some custom checks afterwards. - assert.throws(() => { throw err; }, { - message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + - 'wrong tag', - library: 'asn1 encoding routines', - function: 'asn1_check_tlen', - reason: 'wrong tag', - code: 'ERR_OSSL_ASN1_WRONG_TAG', + // The correct header inside `rsa_private_pkcs8_bad.pem` should have been + // -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY----- + // instead of + // -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- + const sha1_privateKey = fixtures.readKey('rsa_private_pkcs8_bad.pem', + 'ascii'); + + if (process.features.openssl_is_boringssl) { + // BoringSSL accepts the PKCS#8 payload despite the legacy PEM label. + const signature = crypto.createSign('sha1').sign(sha1_privateKey); + assert(Buffer.isBuffer(signature)); + assert.strictEqual(signature.length, 256); + } else { + assert.throws(() => { + // This would inject errors onto OpenSSL's error stack + crypto.createSign('sha1').sign(sha1_privateKey); + }, (err) => { + // Do the standard checks, but then do some custom checks afterwards. + assert.throws(() => { throw err; }, { + message: 'error:0D0680A8:asn1 encoding routines:asn1_check_tlen:' + + 'wrong tag', + library: 'asn1 encoding routines', + function: 'asn1_check_tlen', + reason: 'wrong tag', + code: 'ERR_OSSL_ASN1_WRONG_TAG', + }); + // Throws crypto error, so there is an opensslErrorStack property. + // The openSSL stack should have content. + assert(Array.isArray(err.opensslErrorStack)); + assert(err.opensslErrorStack.length > 0); + return true; }); - // Throws crypto error, so there is an opensslErrorStack property. - // The openSSL stack should have content. - assert(Array.isArray(err.opensslErrorStack)); - assert(err.opensslErrorStack.length > 0); - return true; - }); + } } // Make sure memory isn't released before being returned diff --git a/test/parallel/test-eslint-no-cryptokey-public-accessors.js b/test/parallel/test-eslint-no-cryptokey-public-accessors.js new file mode 100644 index 00000000000000..1b0cd7f930f569 --- /dev/null +++ b/test/parallel/test-eslint-no-cryptokey-public-accessors.js @@ -0,0 +1,88 @@ +'use strict'; + +const common = require('../common'); +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/no-cryptokey-public-accessors'); + +new RuleTester().run('no-cryptokey-public-accessors', rule, { + valid: [ + 'foo.algorithm;', + ` + const { isCryptoKey, getCryptoKeyAlgorithm } = + require('internal/crypto/keys'); + if (isCryptoKey(key)) { + getCryptoKeyAlgorithm(key); + } + `, + ` + const { CryptoKey } = require('internal/crypto/keys'); + class Key extends CryptoKey { + get type() { return 'secret'; } + } + `, + ` + key = webidl.converters.KeyFormat(key); + key.algorithm; + `, + ], + invalid: [ + { + code: ` + const { isCryptoKey } = require('internal/crypto/keys'); + if (isCryptoKey(key)) { + key.type; + } + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const { isCryptoKey: check } = require('internal/crypto/keys'); + if (check(key) && key.extractable) {} + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const { isCryptoKey } = require('internal/crypto/keys'); + if (!isCryptoKey(key)) { + throw new TypeError(); + } + key.algorithm.name; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const keys = require('internal/crypto/keys'); + if (!keys.isCryptoKey(key)) throw new TypeError(); + key['usages']; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + key = webidl.converters.CryptoKey(key); + key.algorithm; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const key = webidl.converters.CryptoKey(value); + key.usages; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + class CryptoKey { + inspect() { return this.algorithm; } + } + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + ], +}); diff --git a/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js b/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js new file mode 100644 index 00000000000000..29b70c9ec58ab8 --- /dev/null +++ b/test/parallel/test-eslint-no-keyobject-cryptokey-instanceof.js @@ -0,0 +1,78 @@ +'use strict'; + +const common = require('../common'); +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/no-keyobject-cryptokey-instanceof'); + +new RuleTester().run('no-keyobject-cryptokey-instanceof', rule, { + valid: [ + 'key instanceof Buffer;', + 'key instanceof KeyObject;', + ` + const { isKeyObject } = require('internal/crypto/keys'); + isKeyObject(key); + `, + ` + const { isCryptoKey } = require('internal/crypto/keys'); + isCryptoKey(key); + `, + ], + invalid: [ + { + code: ` + const { KeyObject } = require('internal/crypto/keys'); + key instanceof KeyObject; + `, + errors: [{ messageId: 'noKeyObjectInstanceof' }], + }, + { + code: ` + const { KeyObject: KO } = require('internal/crypto/keys'); + key instanceof KO; + `, + errors: [{ messageId: 'noKeyObjectInstanceof' }], + }, + { + code: ` + const keys = require('internal/crypto/keys'); + key instanceof keys.KeyObject; + `, + errors: [{ messageId: 'noKeyObjectInstanceof' }], + }, + { + code: ` + key instanceof CryptoKey; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: ` + const { CryptoKey } = require('internal/crypto/keys'); + key instanceof CryptoKey; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: ` + const { CryptoKey: CK } = require('internal/crypto/webcrypto'); + key instanceof CK; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: ` + const webcrypto = require('internal/crypto/webcrypto'); + key instanceof webcrypto.CryptoKey; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + { + code: ` + key instanceof globalThis.CryptoKey; + `, + errors: [{ messageId: 'noCryptoKeyInstanceof' }], + }, + ], +}); diff --git a/test/parallel/test-eslint-no-keyobject-public-accessors.js b/test/parallel/test-eslint-no-keyobject-public-accessors.js new file mode 100644 index 00000000000000..2420ae48763995 --- /dev/null +++ b/test/parallel/test-eslint-no-keyobject-public-accessors.js @@ -0,0 +1,85 @@ +'use strict'; + +const common = require('../common'); +common.skipIfEslintMissing(); + +const RuleTester = require('../../tools/eslint/node_modules/eslint').RuleTester; +const rule = require('../../tools/eslint-rules/no-keyobject-public-accessors'); + +new RuleTester().run('no-keyobject-public-accessors', rule, { + valid: [ + 'foo.type;', + ` + const { isKeyObject, getKeyObjectType } = + require('internal/crypto/keys'); + if (isKeyObject(key)) { + getKeyObjectType(key); + } + `, + ` + const { isKeyObject } = require('internal/crypto/keys'); + if (format === 'raw-public') { + key.asymmetricKeyType; + } + `, + ` + const { KeyObject } = require('internal/crypto/keys'); + class Key extends KeyObject { + get type() { return 'secret'; } + } + `, + ], + invalid: [ + { + code: ` + const { isKeyObject } = require('internal/crypto/keys'); + if (isKeyObject(key)) { + key.type; + } + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const { isKeyObject: check } = require('internal/crypto/keys'); + if (check(key) && key.symmetricKeySize === 32) {} + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const { isKeyObject } = require('internal/crypto/keys'); + if (!isKeyObject(otherKeyObject)) { + throw new TypeError(); + } + otherKeyObject.asymmetricKeyType; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const keys = require('internal/crypto/keys'); + if (!keys.isKeyObject(otherKeyObject)) throw new TypeError(); + otherKeyObject.asymmetricKeyDetails; + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + class SecretKeyObject extends KeyObject { + export() { return this.symmetricKeySize; } + } + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + { + code: ` + const { isKeyObject } = require('internal/crypto/keys'); + if (isKeyObject(key)) { + key.equals(otherKey); + } + `, + errors: [{ messageId: 'noPublicAccessor' }], + }, + ], +}); diff --git a/test/parallel/test-https-agent-session-reuse.js b/test/parallel/test-https-agent-session-reuse.js index 485f4b1ca308c9..c5b7b78b8e0272 100644 --- a/test/parallel/test-https-agent-session-reuse.js +++ b/test/parallel/test-https-agent-session-reuse.js @@ -5,6 +5,11 @@ const assert = require('assert'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const https = require('https'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-https-client-renegotiation-limit.js b/test/parallel/test-https-client-renegotiation-limit.js index 6614090e737614..729176b7c1aa21 100644 --- a/test/parallel/test-https-client-renegotiation-limit.js +++ b/test/parallel/test-https-client-renegotiation-limit.js @@ -25,6 +25,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const https = require('https'); diff --git a/test/parallel/test-https-foafssl.js b/test/parallel/test-https-foafssl.js index ffa44f218b935d..a191bdcf32b73e 100644 --- a/test/parallel/test-https-foafssl.js +++ b/test/parallel/test-https-foafssl.js @@ -56,8 +56,8 @@ const server = https.createServer(options, common.mustCall(function(req, res) { cert = req.connection.getPeerCertificate(); assert.strictEqual(cert.subjectaltname, webIdUrl); - assert.strictEqual(cert.exponent, exponent); - assert.strictEqual(cert.modulus, modulus); + assert.strictEqual(cert.exponent.toLowerCase(), exponent.toLowerCase()); + assert.strictEqual(cert.modulus.toLowerCase(), modulus.toLowerCase()); res.writeHead(200, { 'content-type': 'text/plain' }); res.end(body, () => { console.log('stream finished'); }); console.log('sent response'); diff --git a/test/parallel/test-https-options-boolean-check.js b/test/parallel/test-https-options-boolean-check.js index 9740704e169f1e..fa02a165b80f10 100644 --- a/test/parallel/test-https-options-boolean-check.js +++ b/test/parallel/test-https-options-boolean-check.js @@ -40,9 +40,23 @@ const keyDataView = toDataView(keyBuff); const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); +function filterBoringSSLKeyCertArrayCases(options, setName) { + if (!process.features.openssl_is_boringssl) + return options; + + // The array-valued cases exercise multi-identity key/cert handling. + // BoringSSL may reject those cases with backend key/cert mismatch errors + // before the boolean/type validation this test is targeting. Keep the scalar + // cases so https.createServer() option type validation is still covered. + common.printSkipMessage( + `BoringSSL: skipping ${setName} key/cert array cases`); + return options.filter(([key, cert]) => !Array.isArray(key) && + !Array.isArray(cert)); +} + // Checks to ensure https.createServer doesn't throw an error // Format ['key', 'cert'] -[ +const validOptions = [ [keyBuff, certBuff], [false, certBuff], [keyBuff, false], @@ -62,13 +76,16 @@ const caArrDataView = toDataView(caCert); [false, [certStr, certStr2]], [[{ pem: keyBuff }], false], [[{ pem: keyBuff }, { pem: keyBuff }], false], -].forEach(([key, cert]) => { - https.createServer({ key, cert }); -}); +]; + +filterBoringSSLKeyCertArrayCases(validOptions, 'valid') + .forEach(([key, cert]) => { + https.createServer({ key, cert }); + }); // Checks to ensure https.createServer predictably throws an error // Format ['key', 'cert', 'expected message'] -[ +const invalidKeyOptions = [ [true, certBuff], [true, certStr], [true, certArrBuff], @@ -81,7 +98,10 @@ const caArrDataView = toDataView(caCert); [[true, keyStr2], [certStr, certStr2], 0], [[true, false], [certBuff, certBuff2], 0], [true, [certBuff, certBuff2]], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidKeyOptions, 'invalid key')) { const val = index === undefined ? key : key[index]; assert.throws(() => { https.createServer({ key, cert }); @@ -92,9 +112,9 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} -[ +const invalidCertOptions = [ [keyBuff, true], [keyStr, true], [keyArrBuff, true], @@ -107,7 +127,10 @@ const caArrDataView = toDataView(caCert); [[keyStr, keyStr2], [certStr, true], 1], [[keyStr, keyStr2], [true, false], 0], [[keyStr, keyStr2], true], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidCertOptions, 'invalid cert')) { const val = index === undefined ? cert : cert[index]; assert.throws(() => { https.createServer({ key, cert }); @@ -118,7 +141,7 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} // Checks to ensure https.createServer works with the CA parameter // Format ['key', 'cert', 'ca'] diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js index 2fb529edcde1b7..9e522d7d7b8a6e 100644 --- a/test/parallel/test-internal-webidl-buffer-source.js +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -4,6 +4,7 @@ require('../common'); const assert = require('assert'); const { test } = require('node:test'); +const vm = require('vm'); const { converters } = require('internal/webidl'); @@ -15,6 +16,18 @@ const TYPED_ARRAY_CTORS = [ BigInt64Array, BigUint64Array, ]; +function createGrowableSharedArrayBufferView(Ctor) { + const buffer = createGrowableSharedArrayBuffer(); + const view = new Ctor(buffer); + return view; +} + +function createGrowableSharedArrayBuffer() { + const buffer = new SharedArrayBuffer(0, { maxByteLength: 1 }); + assert.strictEqual(buffer.growable, true); + return buffer; +} + test('BufferSource accepts ArrayBuffer', () => { const ab = new ArrayBuffer(8); assert.strictEqual(converters.BufferSource(ab), ab); @@ -37,6 +50,28 @@ test('BufferSource accepts DataView', () => { assert.strictEqual(converters.BufferSource(dv), dv); }); +test('BufferSource accepts cross-realm buffer sources', () => { + const context = vm.createContext(); + + { + const ab = vm.runInContext('new ArrayBuffer(0)', context); + assert.strictEqual(converters.BufferSource(ab), ab); + } + + { + const dv = vm.runInContext('new DataView(new ArrayBuffer(0))', context); + assert.strictEqual(converters.BufferSource(dv), dv); + } + + for (const Ctor of TYPED_ARRAY_CTORS) { + const ta = vm.runInContext( + `new ${Ctor.name}(new ArrayBuffer(0))`, + context, + ); + assert.strictEqual(converters.BufferSource(ta), ta); + } +}); + test('BufferSource accepts ArrayBuffer subclass instance', () => { class MyAB extends ArrayBuffer {} const sub = new MyAB(8); @@ -74,6 +109,10 @@ test('BufferSource rejects SAB-backed TypedArray', () => { () => converters.BufferSource(view), { code: 'ERR_INVALID_ARG_TYPE' }, ); + assert.throws( + () => converters.BufferSource(view, { allowShared: true }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); }); test('BufferSource rejects SAB-backed DataView', () => { @@ -82,6 +121,10 @@ test('BufferSource rejects SAB-backed DataView', () => { () => converters.BufferSource(dv), { code: 'ERR_INVALID_ARG_TYPE' }, ); + assert.throws( + () => converters.BufferSource(dv, { allowShared: true }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); }); test('BufferSource rejects SAB view whose buffer prototype was reassigned', () => { @@ -101,107 +144,154 @@ test('BufferSource accepts a detached ArrayBuffer', () => { assert.strictEqual(converters.BufferSource(ab), ab); }); -test('BufferSource rejects objects with a forged @@toStringTag', () => { - const fake = { [Symbol.toStringTag]: 'Uint8Array' }; +test('BufferSource rejects resizable ArrayBuffer by default', () => { + const ab = new ArrayBuffer(0, { maxByteLength: 1 }); assert.throws( - () => converters.BufferSource(fake), + () => converters.BufferSource(ab), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [], - {}, () => {}]) { - test(`BufferSource rejects ${typeof value} ${String(value)}`, () => { +test('BufferSource handles resizable-backed views with explicit options', () => { + for (const Ctor of [DataView, ...TYPED_ARRAY_CTORS]) { + { + const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 })); + assert.throws( + () => converters.BufferSource(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + + { + const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 })); + assert.throws( + () => converters.BufferSource(view, { allowResizable: false }), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + + { + const view = new Ctor(new ArrayBuffer(0, { maxByteLength: 1 })); + assert.strictEqual(converters.BufferSource(view, { + allowResizable: true, + }), view); + } + } +}); + +test('BufferSource rejects SAB-backed views with explicit options', () => { + for (const Ctor of [DataView, ...TYPED_ARRAY_CTORS]) { + const view = createGrowableSharedArrayBufferView(Ctor); assert.throws( - () => converters.BufferSource(value), + () => converters.BufferSource(view, { + allowShared: true, + allowResizable: true, + }), { code: 'ERR_INVALID_ARG_TYPE' }, ); - }); -} - -test('ArrayBufferView accepts all TypedArray kinds', () => { - for (const Ctor of TYPED_ARRAY_CTORS) { - const ta = new Ctor(4); - assert.strictEqual(converters.ArrayBufferView(ta), ta); } }); -test('ArrayBufferView accepts DataView', () => { - const dv = new DataView(new ArrayBuffer(8)); - assert.strictEqual(converters.ArrayBufferView(dv), dv); -}); +test('AllowSharedBufferSource accepts ArrayBuffer and SharedArrayBuffer', () => { + const ab = new ArrayBuffer(8); + const sab = new SharedArrayBuffer(8); -test('ArrayBufferView accepts TypedArray subclass instance', () => { - class MyU8 extends Uint8Array {} - const sub = new MyU8(4); - assert.strictEqual(converters.ArrayBufferView(sub), sub); + assert.strictEqual(converters.AllowSharedBufferSource(ab), ab); + assert.strictEqual(converters.AllowSharedBufferSource(sab), sab); }); -test('ArrayBufferView accepts TypedArray with null prototype', () => { - const ta = new Uint8Array(4); - Object.setPrototypeOf(ta, null); - assert.strictEqual(converters.ArrayBufferView(ta), ta); -}); +test('AllowSharedBufferSource accepts cross-realm buffers', () => { + const context = vm.createContext(); + const ab = vm.runInContext('new ArrayBuffer(0)', context); + const sab = vm.runInContext('new SharedArrayBuffer(0)', context); -test('ArrayBufferView accepts DataView with null prototype', () => { - const dv = new DataView(new ArrayBuffer(4)); - Object.setPrototypeOf(dv, null); - assert.strictEqual(converters.ArrayBufferView(dv), dv); + assert.strictEqual(converters.AllowSharedBufferSource(ab), ab); + assert.strictEqual(converters.AllowSharedBufferSource(sab), sab); }); -test('ArrayBufferView rejects raw ArrayBuffer', () => { - assert.throws( - () => converters.ArrayBufferView(new ArrayBuffer(4)), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); -}); +test('AllowSharedBufferSource accepts ArrayBuffer and SharedArrayBuffer views', () => { + const abView = new Uint8Array(new ArrayBuffer(8)); + const sabView = new Uint8Array(new SharedArrayBuffer(8)); + const abDataView = new DataView(new ArrayBuffer(8)); + const sabDataView = new DataView(new SharedArrayBuffer(8)); -test('ArrayBufferView rejects raw SharedArrayBuffer', () => { - assert.throws( - () => converters.ArrayBufferView(new SharedArrayBuffer(4)), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); + assert.strictEqual(converters.AllowSharedBufferSource(abView), abView); + assert.strictEqual(converters.AllowSharedBufferSource(sabView), sabView); + assert.strictEqual( + converters.AllowSharedBufferSource(abDataView), abDataView); + assert.strictEqual( + converters.AllowSharedBufferSource(sabDataView), sabDataView); }); -test('ArrayBufferView rejects SAB-backed TypedArray', () => { - const view = new Uint8Array(new SharedArrayBuffer(4)); - assert.throws( - () => converters.ArrayBufferView(view), - { code: 'ERR_INVALID_ARG_TYPE' }, - ); -}); +test('AllowSharedBufferSource handles resizable buffers with explicit options', () => { + const ab = new ArrayBuffer(0, { maxByteLength: 1 }); + const views = [new Uint8Array(ab), new DataView(ab)]; -test('ArrayBufferView rejects SAB-backed DataView', () => { - const dv = new DataView(new SharedArrayBuffer(4)); assert.throws( - () => converters.ArrayBufferView(dv), + () => converters.AllowSharedBufferSource(ab), { code: 'ERR_INVALID_ARG_TYPE' }, ); + for (const view of views) { + assert.throws( + () => converters.AllowSharedBufferSource(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + assert.strictEqual(converters.AllowSharedBufferSource(ab, { + allowResizable: true, + }), ab); + for (const view of views) { + assert.strictEqual(converters.AllowSharedBufferSource(view, { + allowResizable: true, + }), view); + } }); -test('ArrayBufferView rejects SAB view whose buffer prototype was reassigned', () => { - const sab = new SharedArrayBuffer(4); - Object.setPrototypeOf(sab, ArrayBuffer.prototype); - const view = new Uint8Array(sab); +test('AllowSharedBufferSource handles growable shared buffers with explicit ' + + 'options', () => { + const sab = createGrowableSharedArrayBuffer(); + const views = [new Uint8Array(sab), new DataView(sab)]; + assert.throws( - () => converters.ArrayBufferView(view), + () => converters.AllowSharedBufferSource(sab), { code: 'ERR_INVALID_ARG_TYPE' }, ); + for (const view of views) { + assert.throws( + () => converters.AllowSharedBufferSource(view), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + } + assert.strictEqual(converters.AllowSharedBufferSource(sab, { + allowResizable: true, + }), sab); + for (const view of views) { + assert.strictEqual(converters.AllowSharedBufferSource(view, { + allowResizable: true, + }), view); + } }); -test('ArrayBufferView rejects objects with a forged @@toStringTag', () => { +test('BufferSource rejects objects with a forged @@toStringTag', () => { const fake = { [Symbol.toStringTag]: 'Uint8Array' }; assert.throws( - () => converters.ArrayBufferView(fake), + () => converters.BufferSource(fake), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); for (const value of [null, undefined, 0, 1, 1n, '', 'x', true, Symbol('s'), [], {}, () => {}]) { - test(`ArrayBufferView rejects ${typeof value} ${String(value)}`, () => { + test(`BufferSource rejects ${typeof value} ${String(value)}`, () => { + assert.throws( + () => converters.BufferSource(value), + { code: 'ERR_INVALID_ARG_TYPE' }, + ); + }); + + test(`AllowSharedBufferSource rejects ${typeof value} ${String(value)}`, () => { assert.throws( - () => converters.ArrayBufferView(value), + () => converters.AllowSharedBufferSource(value), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); diff --git a/test/parallel/test-internal-webidl-converttoint.js b/test/parallel/test-internal-webidl-converttoint.js index 7e7c024387a0ec..4c2032dc8354d1 100644 --- a/test/parallel/test-internal-webidl-converttoint.js +++ b/test/parallel/test-internal-webidl-converttoint.js @@ -3,56 +3,360 @@ require('../common'); const assert = require('assert'); -const { convertToInt, evenRound } = require('internal/webidl'); +const { convertToInt } = require('internal/webidl'); -assert.strictEqual(evenRound(-0.5), 0); -assert.strictEqual(evenRound(0.5), 0); -assert.strictEqual(evenRound(-1.5), -2); -assert.strictEqual(evenRound(1.5), 2); -assert.strictEqual(evenRound(3.4), 3); -assert.strictEqual(evenRound(4.6), 5); -assert.strictEqual(evenRound(5), 5); -assert.strictEqual(evenRound(6), 6); +function assertPlainTypeError(fn) { + assert.throws(fn, (err) => { + assert(err instanceof TypeError); + assert.strictEqual(err.name, 'TypeError'); + assert.strictEqual(err.code, undefined); + return true; + }); +} + +function assertSameError(fn, expected) { + assert.throws(fn, (err) => { + assert.strictEqual(err, expected); + return true; + }); +} // https://webidl.spec.whatwg.org/#abstract-opdef-converttoint -assert.strictEqual(convertToInt('x', 0, 64), 0); -assert.strictEqual(convertToInt('x', 1, 64), 1); -assert.strictEqual(convertToInt('x', -0.5, 64), 0); -assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true }), 0); -assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true }), -1); +const two63 = 2 ** 63; +const two64 = 2 ** 64; + +assert.strictEqual(convertToInt(0, 64), 0); +assert.strictEqual(convertToInt(1, 64), 1); +assert.strictEqual(convertToInt(-0.5, 64), 0); +assert.strictEqual(convertToInt(-0.5, 64, 'signed'), 0); +assert.strictEqual(convertToInt(-1.5, 64, 'signed'), -1); +assert.strictEqual(convertToInt(2 ** 12 + 1, 12), 1); +assert.strictEqual(convertToInt(-1, 12), 2 ** 12 - 1); +assert.strictEqual(convertToInt(2 ** 11, 12, 'signed'), -(2 ** 11)); +assert.strictEqual(convertToInt(-1, 12, 'signed'), -1); + +{ + const options = { + get enforceRange() { + throw new Error('enforceRange should not be read'); + }, + get clamp() { + throw new Error('clamp should not be read'); + }, + }; + + assert.strictEqual(convertToInt(7, 8, 'unsigned', options), 7); +} + +{ + const opts = { + __proto__: null, + prefix: 'Prefix', + context: 'Context', + }; + + assert.throws(() => convertToInt(1n, 8, 'unsigned', opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a BigInt and cannot be converted ' + + 'to a number.', + }); + + assert.throws(() => convertToInt(Symbol(), 8, 'unsigned', opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a Symbol and cannot be converted ' + + 'to a number.', + }); + + for (const value of [ + Object(1n), + { valueOf() { return 1n; } }, + { valueOf() { return {}; }, toString() { return 1n; } }, + { [Symbol.toPrimitive]() { return 1n; } }, + Object(Symbol()), + { valueOf() { return Symbol(); } }, + { valueOf() { return {}; }, toString() { return Symbol(); } }, + { [Symbol.toPrimitive]() { return Symbol(); } }, + ]) { + assertPlainTypeError(() => convertToInt(value, 8, 'unsigned', opts)); + } + + assert.strictEqual(convertToInt({ + valueOf() { return {}; }, + toString() { return 7; }, + }, 8), 7); + + { + const value = Object(1n); + value.valueOf = () => 7; + assert.strictEqual(convertToInt(value, 8), 7); + } + + { + const calls = []; + const value = { + [Symbol.toPrimitive](hint) { + calls.push(hint); + return '7'; + }, + valueOf() { + calls.push('valueOf'); + return 1; + }, + toString() { + calls.push('toString'); + return '1'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 7); + assert.deepStrictEqual(calls, ['number']); + } + + for (const { value, expected } of [ + { + value: { + [Symbol.toPrimitive]: undefined, + valueOf() { return 8; }, + }, + expected: 8, + }, + { + value: { + [Symbol.toPrimitive]: null, + valueOf() { return 9; }, + }, + expected: 9, + }, + ]) { + assert.strictEqual(convertToInt(value, 8), expected); + } + + { + const calls = []; + const value = { + valueOf: 1, + toString() { + calls.push('toString'); + return '10'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 10); + assert.deepStrictEqual(calls, ['toString']); + } + + { + const calls = []; + const value = { + valueOf() { + calls.push('valueOf'); + return {}; + }, + toString() { + calls.push('toString'); + return '11'; + }, + }; + + assert.strictEqual(convertToInt(value, 8), 11); + assert.deepStrictEqual(calls, ['valueOf', 'toString']); + } + + for (const value of [ + { [Symbol.toPrimitive]: 1 }, + { [Symbol.toPrimitive]() { return {}; } }, + { + valueOf() { return {}; }, + toString() { return {}; }, + }, + ]) { + assertPlainTypeError(() => convertToInt(value, 8, 'unsigned', opts)); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + get [Symbol.toPrimitive]() { + throw sentinel; + }, + }, 8), sentinel); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + get valueOf() { + throw sentinel; + }, + toString() { + return 1; + }, + }, 8), sentinel); + } + + { + const sentinel = new TypeError('sentinel'); + assertSameError(() => convertToInt({ + valueOf() { + throw sentinel; + }, + toString() { + return 1; + }, + }, 8), sentinel); + } +} // EnforceRange -const OutOfRangeValues = [ NaN, Infinity, -Infinity, 2 ** 53, -(2 ** 53) ]; -for (const value of OutOfRangeValues) { - assert.throws(() => convertToInt('x', value, 64, { enforceRange: true }), { +const nonFiniteValues = [NaN, Infinity, -Infinity]; +for (const value of nonFiniteValues) { + assert.throws(() => convertToInt(value, 64, 'unsigned', { + enforceRange: true, + }), { name: 'TypeError', - code: 'ERR_INVALID_ARG_VALUE', + code: 'ERR_INVALID_ARG_TYPE', }); } +assert.strictEqual(convertToInt(-0.8, 64, 'unsigned', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(-0.8, 64, 'signed', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(Number.MAX_SAFE_INTEGER, 64, 'signed', { + enforceRange: true, +}), Number.MAX_SAFE_INTEGER); +assert.strictEqual(convertToInt(Number.MIN_SAFE_INTEGER, 64, 'signed', { + enforceRange: true, +}), Number.MIN_SAFE_INTEGER); +assert.strictEqual(convertToInt(-0.5, 8, 'unsigned', { + enforceRange: true, +}), 0); +assert.strictEqual(convertToInt(255.5, 8, 'unsigned', { + enforceRange: true, +}), 255); + +const outOfRangeValues = [2 ** 53, -(2 ** 53)]; +for (const value of outOfRangeValues) { + assert.throws(() => convertToInt(value, 64, 'unsigned', { + enforceRange: true, + }), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', + }); +} +assert.throws(() => convertToInt(Number.MAX_SAFE_INTEGER + 1, 64, 'signed', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => convertToInt(Number.MIN_SAFE_INTEGER - 1, 64, 'signed', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => convertToInt(256, 8, 'unsigned', { + enforceRange: true, +}), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', +}); + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return false; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt(256, 8, 'unsigned', options), 255); + assert.deepStrictEqual(calls, ['enforceRange', 'clamp']); +} + +{ + const calls = []; + const options = { + get enforceRange() { + calls.push('enforceRange'); + return false; + }, + get clamp() { + calls.push('clamp'); + return true; + }, + }; + + assert.strictEqual(convertToInt({ valueOf: () => 2.5 }, 8, 'unsigned', options), 2); + assert.deepStrictEqual(calls, ['enforceRange', 'clamp']); +} + // Out of range: clamp -assert.strictEqual(convertToInt('x', NaN, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', Infinity, 64, { clamp: true }), Number.MAX_SAFE_INTEGER); -assert.strictEqual(convertToInt('x', -Infinity, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', -Infinity, 64, { signed: true, clamp: true }), Number.MIN_SAFE_INTEGER); -assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32, { clamp: true }), 0xFFFF_FFFF); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true }), 0xFFFF_FFFF); -assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { clamp: true, signed: true }), 0x7FFF_FFFF); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32, { clamp: true, signed: true }), 0x7FFF_FFFF); -assert.strictEqual(convertToInt('x', 0.5, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', 1.5, 64, { clamp: true }), 2); -assert.strictEqual(convertToInt('x', -0.5, 64, { clamp: true }), 0); -assert.strictEqual(convertToInt('x', -0.5, 64, { signed: true, clamp: true }), 0); -assert.strictEqual(convertToInt('x', -1.5, 64, { signed: true, clamp: true }), -2); +assert.strictEqual(convertToInt(NaN, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(Infinity, 64, 'unsigned', { clamp: true }), Number.MAX_SAFE_INTEGER); +assert.strictEqual(convertToInt(-Infinity, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(-Infinity, 64, 'signed', { clamp: true }), Number.MIN_SAFE_INTEGER); +assert.strictEqual(convertToInt(0x1_0000, 16, 'unsigned', { clamp: true }), 0xFFFF); +assert.strictEqual(convertToInt(0x1_0000_0000, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'unsigned', { clamp: true }), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(0x8000, 16, 'signed', { clamp: true }), 0x7FFF); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed', { clamp: true }), -0x8000); +assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed', { clamp: true }), 0x7FFF_FFFF); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32, 'signed', { clamp: true }), 0x7FFF_FFFF); +assert.strictEqual(convertToInt(0.5, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(0.8, 64, 'unsigned', { clamp: true }), 1); +assert.strictEqual(convertToInt(1.5, 64, 'unsigned', { clamp: true }), 2); +assert.strictEqual(convertToInt(-0.5, 64, 'unsigned', { clamp: true }), 0); +assert.strictEqual(convertToInt(-0.5, 64, 'signed', { clamp: true }), 0); +assert.strictEqual(convertToInt(-0.8, 64, 'signed', { clamp: true }), -1); +assert.strictEqual(convertToInt(-1.5, 64, 'signed', { clamp: true }), -2); // Out of range, step 8. -assert.strictEqual(convertToInt('x', NaN, 64), 0); -assert.strictEqual(convertToInt('x', Infinity, 64), 0); -assert.strictEqual(convertToInt('x', -Infinity, 64), 0); -assert.strictEqual(convertToInt('x', 0x1_0000_0000, 32), 0); -assert.strictEqual(convertToInt('x', 0x1_0000_0001, 32), 1); -assert.strictEqual(convertToInt('x', 0xFFFF_FFFF, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(NaN, 64), 0); +assert.strictEqual(convertToInt(Infinity, 64), 0); +assert.strictEqual(convertToInt(-Infinity, 64), 0); +assert.strictEqual(convertToInt(-3, 8), 253); +assert.strictEqual(convertToInt(-3, 8), new Uint8Array([-3])[0]); +assert.strictEqual(convertToInt(0x1_0000, 16), 0); +assert.strictEqual(convertToInt(0x1_0001, 16), 1); +assert.strictEqual(convertToInt(-1, 16), 0xFFFF); +assert.strictEqual(convertToInt(-1, 16), new Uint16Array([-1])[0]); +assert.strictEqual(convertToInt(0x1_0000_0000, 32), 0); +assert.strictEqual(convertToInt(0x1_0000_0001, 32), 1); +assert.strictEqual(convertToInt(0xFFFF_FFFF, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(-1, 32), 0xFFFF_FFFF); +assert.strictEqual(convertToInt(-1, 32), new Uint32Array([-1])[0]); +assert.strictEqual(convertToInt(-8192, 64), 2 ** 64 - 8192); +assert.strictEqual(convertToInt(two63, 64), two63); +assert.strictEqual(convertToInt(two64, 64), 0); +assert.strictEqual(convertToInt(two64 + 8192, 64), 8192); +assert.strictEqual(convertToInt(-(two64 + 2 ** 12), 64), + two64 - 2 ** 12); // Out of range, step 11. -assert.strictEqual(convertToInt('x', 0x8000_0000, 32, { signed: true }), -0x8000_0000); -assert.strictEqual(convertToInt('x', 0xFFF_FFFF, 32, { signed: true }), 0xFFF_FFFF); +assert.strictEqual(convertToInt(0x8000, 16, 'signed'), -0x8000); +assert.strictEqual(convertToInt(0xFFFF, 16, 'signed'), -1); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), 0x7FFF); +assert.strictEqual(convertToInt(-0x8001, 16, 'signed'), new Int16Array([-0x8001])[0]); +assert.strictEqual(convertToInt(0x8000_0000, 32, 'signed'), -0x8000_0000); +assert.strictEqual(convertToInt(0xFFF_FFFF, 32, 'signed'), 0xFFF_FFFF); +assert.strictEqual(convertToInt(-200, 8, 'signed'), 56); +assert.strictEqual(convertToInt(-200, 8, 'signed'), new Int8Array([-200])[0]); +assert.strictEqual(convertToInt(-8192, 64, 'signed'), -8192); +assert.strictEqual(convertToInt(-8193, 64, 'signed'), -8193); +assert.strictEqual(convertToInt(two63, 64, 'signed'), -two63); +assert.strictEqual(convertToInt(two63 + 2 ** 11, 64, 'signed'), + -two63 + 2 ** 11); +assert.strictEqual(convertToInt(two64 + 8192, 64, 'signed'), 8192); +assert.strictEqual(convertToInt(-(two64 + 2 ** 12), 64, 'signed'), + -(2 ** 12)); diff --git a/test/parallel/test-internal-webidl.js b/test/parallel/test-internal-webidl.js new file mode 100644 index 00000000000000..bd08648549be78 --- /dev/null +++ b/test/parallel/test-internal-webidl.js @@ -0,0 +1,616 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const vm = require('vm'); +const webidl = require('internal/webidl'); + +const { converters } = webidl; +const opts = { + __proto__: null, + prefix: 'Prefix', + context: 'Context', +}; +function createGrowableSharedArrayBufferView() { + const buffer = new SharedArrayBuffer(4, { maxByteLength: 8 }); + const view = new Uint8Array(buffer); + assert.strictEqual(view.buffer.growable, true); + return view; +} + +function assertInvalidArgType(fn) { + assert.throws(fn, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); +} + +function assertPlainTypeError(fn) { + assert.throws(fn, (err) => { + assert(err instanceof TypeError); + assert.strictEqual(err.name, 'TypeError'); + assert.strictEqual(err.code, undefined); + return true; + }); +} + +function assertSameError(fn, expected) { + assert.throws(fn, (err) => { + assert.strictEqual(err, expected); + return true; + }); +} + +assert.strictEqual(webidl.type(undefined), 'Undefined'); +assert.strictEqual(webidl.type(null), 'Null'); +assert.strictEqual(webidl.type(false), 'Boolean'); +assert.strictEqual(webidl.type(''), 'String'); +assert.strictEqual(webidl.type(Symbol()), 'Symbol'); +assert.strictEqual(webidl.type(0), 'Number'); +assert.strictEqual(webidl.type(0n), 'BigInt'); +assert.strictEqual(webidl.type({}), 'Object'); +assert.strictEqual(webidl.type(function fn() {}), 'Object'); + +assert.strictEqual(converters.boolean(0), false); +assert.strictEqual(converters.boolean('false'), true); +{ + function fn() {} + assert.strictEqual(converters.object(fn), fn); +} +assert.throws(() => converters.object(null, opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is not an object.', +}); + +assert.strictEqual(converters.DOMString(null), 'null'); +assert.throws(() => converters.DOMString(Symbol(), opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a Symbol and cannot be converted to a string.', +}); + +for (const value of [ + Object(Symbol()), + { toString() { return Symbol(); } }, + { toString() { return {}; }, valueOf() { return Symbol(); } }, + { [Symbol.toPrimitive]() { return Symbol(); } }, +]) { + assertPlainTypeError(() => converters.DOMString(value, opts)); +} +assert.strictEqual(converters.DOMString(1n), '1'); +assert.strictEqual(converters.DOMString(Object(1n)), '1'); +assert.strictEqual(converters.DOMString({ + toString() { return {}; }, + valueOf() { return 1n; }, +}), '1'); +assert.strictEqual(converters.DOMString({ + [Symbol.toPrimitive]() { return 1n; }, +}), '1'); +{ + const value = Object(Symbol()); + Object.defineProperty(value, Symbol.toPrimitive, { + __proto__: null, + value() { return 'symbol object'; }, + }); + assert.strictEqual(converters.DOMString(value), 'symbol object'); +} + +{ + const calls = []; + const value = { + [Symbol.toPrimitive](hint) { + calls.push(hint); + return 7; + }, + toString() { + calls.push('toString'); + return '1'; + }, + valueOf() { + calls.push('valueOf'); + return 1; + }, + }; + + assert.strictEqual(converters.DOMString(value), '7'); + assert.deepStrictEqual(calls, ['string']); +} + +for (const { value, expected } of [ + { + value: { + [Symbol.toPrimitive]: undefined, + toString() { return 'eight'; }, + }, + expected: 'eight', + }, + { + value: { + [Symbol.toPrimitive]: null, + toString() { return 'nine'; }, + }, + expected: 'nine', + }, +]) { + assert.strictEqual(converters.DOMString(value), expected); +} + +{ + const calls = []; + const value = { + toString: 1, + valueOf() { + calls.push('valueOf'); + return 10; + }, + }; + + assert.strictEqual(converters.DOMString(value), '10'); + assert.deepStrictEqual(calls, ['valueOf']); +} + +{ + const calls = []; + const value = { + toString() { + calls.push('toString'); + return {}; + }, + valueOf() { + calls.push('valueOf'); + return 11; + }, + }; + + assert.strictEqual(converters.DOMString(value), '11'); + assert.deepStrictEqual(calls, ['toString', 'valueOf']); +} + +for (const value of [ + { [Symbol.toPrimitive]: 1 }, + { [Symbol.toPrimitive]() { return {}; } }, + { + toString() { return {}; }, + valueOf() { return {}; }, + }, +]) { + assertPlainTypeError(() => converters.DOMString(value, opts)); +} + +{ + const sentinel = new TypeError('sentinel'); + assertSameError(() => converters.DOMString({ + get [Symbol.toPrimitive]() { + throw sentinel; + }, + }), sentinel); +} + +{ + const sentinel = new TypeError('sentinel'); + assertSameError(() => converters.DOMString({ + get toString() { + throw sentinel; + }, + valueOf() { + return 1; + }, + }), sentinel); +} + +{ + const sentinel = new TypeError('sentinel'); + assertSameError(() => converters.DOMString({ + toString() { + throw sentinel; + }, + valueOf() { + return 1; + }, + }), sentinel); +} + +{ + assert.strictEqual(converters.octet(-1), 255); + assert.strictEqual(converters['unsigned short'](-1), 0xFFFF); + assert.strictEqual(converters['unsigned long'](-1), 0xFFFF_FFFF); + assert.strictEqual(converters['long long'](-1), -1); + assert.strictEqual(converters.octet(2.5, { + __proto__: null, + clamp: true, + }), 2); + assert.throws(() => converters.octet(256, { + __proto__: null, + ...opts, + enforceRange: true, + }), { + name: 'TypeError', + code: 'ERR_OUT_OF_RANGE', + message: 'Prefix: Context is outside the expected range of 0 to 255.', + }); +} + +{ + const converter = webidl.createEnumConverter('Example', ['one', 'two']); + + assert.strictEqual(converter('one'), 'one'); + assert.throws(() => converter(Symbol(), opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is a Symbol and cannot be converted to a string.', + }); + assert.throws(() => converter('three', opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_VALUE', + message: "Prefix: Context 'three' is not a valid enum value " + + 'of type Example.', + }); +} + +assert.throws(() => webidl.requiredArguments(1, 2, opts), { + name: 'TypeError', + code: 'ERR_MISSING_ARGS', + message: 'Prefix: 2 arguments required, but only 1 present.', +}); + +{ + const ab = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(ab); + const dataView = new DataView(ab); + + assert.strictEqual(ab.resizable, true); + assertInvalidArgType(() => converters.BufferSource(ab)); + assertInvalidArgType(() => converters.BufferSource(view)); + assertInvalidArgType(() => converters.BufferSource(dataView)); + assertInvalidArgType(() => converters.Uint8Array(view)); + + const disallowResizable = { + __proto__: null, + allowResizable: false, + }; + assertInvalidArgType(() => converters.BufferSource(ab, disallowResizable)); + assertInvalidArgType(() => converters.BufferSource(view, disallowResizable)); + assertInvalidArgType(() => converters.BufferSource(dataView, + disallowResizable)); + + const allowResizable = { + __proto__: null, + allowResizable: true, + }; + assert.strictEqual(converters.BufferSource(ab, allowResizable), ab); + assert.strictEqual(converters.BufferSource(view, allowResizable), view); + assert.strictEqual(converters.BufferSource(dataView, allowResizable), + dataView); + assert.strictEqual(converters.Uint8Array(view, allowResizable), view); +} + +{ + const ab = new ArrayBuffer(8); + const view = new Uint8Array(ab); + const dataView = new DataView(ab); + + structuredClone(ab, { transfer: [ab] }); + assert.strictEqual(ab.detached, true); + assert.strictEqual(converters.BufferSource(ab), ab); + assert.strictEqual(converters.BufferSource(view), view); + assert.strictEqual(converters.BufferSource(dataView), dataView); +} + +{ + const ab = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(ab); + const dataView = new DataView(ab); + const allowResizable = { + __proto__: null, + allowResizable: true, + }; + + structuredClone(ab, { transfer: [ab] }); + assert.strictEqual(ab.detached, true); + assert.strictEqual(ab.resizable, true); + assertInvalidArgType(() => converters.BufferSource(ab)); + assertInvalidArgType(() => converters.BufferSource(view)); + assertInvalidArgType(() => converters.BufferSource(dataView)); + assertInvalidArgType(() => converters.Uint8Array(view)); + + const disallowResizable = { + __proto__: null, + allowResizable: false, + }; + assertInvalidArgType(() => converters.BufferSource(ab, disallowResizable)); + assertInvalidArgType(() => converters.BufferSource(view, disallowResizable)); + assertInvalidArgType(() => converters.BufferSource(dataView, + disallowResizable)); + + assert.strictEqual(converters.BufferSource(ab, allowResizable), ab); + assert.strictEqual(converters.BufferSource(view, allowResizable), view); + assert.strictEqual(converters.BufferSource(dataView, allowResizable), + dataView); + assert.strictEqual(converters.Uint8Array(view, allowResizable), view); +} + +{ + const ab = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(ab); + + assert.strictEqual(converters.BufferSource(ab, { + __proto__: null, + allowResizable: true, + }), ab); + assert.strictEqual(converters.BufferSource(view, { + __proto__: null, + allowResizable: true, + }), view); + + ab.resize(4); + assert.strictEqual(ab.byteLength, 4); + assert.strictEqual(view.byteLength, 4); + + ab.resize(12); + assert.strictEqual(ab.byteLength, 12); + assert.strictEqual(view.byteLength, 12); +} + +{ + const converter = webidl.createDictionaryConverter('Example', [ + { + key: 'value', + converter: converters.DOMString, + required: true, + }, + ]); + + const idlDict = converter({ value: 1 }); + assert.deepStrictEqual(idlDict, { __proto__: null, value: '1' }); + + assert.throws(() => converter({}, opts), { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: "Prefix: Context cannot be converted to 'Example' because " + + "'value' is required in 'Example'.", + }); +} + +{ + const calls = []; + const converter = webidl.createDictionaryConverter('Derived', [ + [ + { + key: 'zBase', + converter(value) { + calls.push(`base z:${value}`); + return value; + }, + }, + { + key: 'aBase', + converter(value) { + calls.push(`base a:${value}`); + return value; + }, + }, + ], + [ + { + key: 'zDerived', + converter(value) { + calls.push(`derived z:${value}`); + return value; + }, + }, + { + key: 'aDerived', + converter(value) { + calls.push(`derived a:${value}`); + return value; + }, + }, + ], + ]); + + assert.deepStrictEqual(converter({ + zBase: 1, + aBase: 2, + zDerived: 3, + aDerived: 4, + }), { + __proto__: null, + aBase: 2, + zBase: 1, + aDerived: 4, + zDerived: 3, + }); + assert.deepStrictEqual(calls, [ + 'base a:2', + 'base z:1', + 'derived a:4', + 'derived z:3', + ]); +} + +{ + const converter = webidl.createDictionaryConverter('Example', [ + { + key: 'same', + converter(value) { + return `first:${value}`; + }, + }, + { + key: 'same', + converter(value) { + return `second:${value}`; + }, + }, + ]); + + assert.deepStrictEqual(converter({ same: 1 }), { + __proto__: null, + same: 'second:1', + }); +} + +{ + const converter = converters['sequence']; + assert.deepStrictEqual(converter([1, 2]), ['1', '2']); + + assert.throws(() => converter([Symbol()]), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Value[0] is a Symbol and cannot be converted to a string.', + }); + + assert.throws(() => converter({ [Symbol.iterator]: 1 }, opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context cannot be converted to sequence.', + }); + + assert.throws(() => converter({ + [Symbol.iterator]() { + return {}; + }, + }, opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context cannot be converted to sequence.', + }); + + assert.throws(() => converter({ + [Symbol.iterator]() { + return { + next() { + return null; + }, + }; + }, + }, opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context cannot be converted to sequence.', + }); + + assert.throws(() => converter([Symbol()], opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context[0] is a Symbol and cannot be converted ' + + 'to a string.', + }); + + assert.deepStrictEqual(converter({ + [Symbol.iterator]() { + return { + next() { + return { done: 1, value: Symbol() }; + }, + }; + }, + }), []); +} + +{ + class Example {} + const converter = webidl.createInterfaceConverter( + 'Example', + Example.prototype); + const example = new Example(); + + assert.strictEqual(converter(example), example); + assert.throws(() => converter({}, opts), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is not of type Example.', + }); +} + +{ + const context = vm.createContext(); + const view = vm.runInContext( + 'new Uint8Array(new ArrayBuffer(0))', + context, + ); + assert.strictEqual(converters.Uint8Array(view), view); + + for (const value of [ + new ArrayBuffer(0), + new SharedArrayBuffer(0), + new DataView(new ArrayBuffer(0)), + new Int8Array(0), + Object.create(Uint8Array.prototype, { + [Symbol.toStringTag]: { value: 'Uint8Array' }, + }), + ]) { + assertInvalidArgType(() => converters.Uint8Array(value)); + } +} + +{ + const sab = new SharedArrayBuffer(4); + const view = new Uint8Array(sab); + + assertInvalidArgType(() => converters.BufferSource(sab)); + assertInvalidArgType(() => converters.BufferSource(view)); + assertInvalidArgType(() => converters.Uint8Array(view)); + + const allowShared = { + __proto__: null, + allowShared: true, + }; + assertInvalidArgType(() => converters.BufferSource(sab, allowShared)); + assertInvalidArgType(() => converters.BufferSource(view, allowShared)); + assert.strictEqual(converters.AllowSharedBufferSource(sab), sab); + assert.strictEqual(converters.AllowSharedBufferSource(view), view); + assert.strictEqual(converters.Uint8Array(view, allowShared), view); + + const growableView = createGrowableSharedArrayBufferView(); + + assertInvalidArgType(() => converters.BufferSource(growableView, { + __proto__: null, + allowShared: true, + allowResizable: true, + })); + assert.throws(() => converters.AllowSharedBufferSource(growableView.buffer, { + __proto__: null, + ...opts, + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is backed by a growable ' + + 'SharedArrayBuffer, which is not allowed.', + }); + assert.throws(() => converters.AllowSharedBufferSource(growableView, { + __proto__: null, + ...opts, + allowResizable: false, + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is backed by a growable ' + + 'SharedArrayBuffer, which is not allowed.', + }); + assert.throws(() => converters.Uint8Array(growableView, { + __proto__: null, + ...opts, + allowShared: true, + }), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'Prefix: Context is backed by a growable ' + + 'SharedArrayBuffer, which is not allowed.', + }); + assert.strictEqual(converters.AllowSharedBufferSource(growableView.buffer, { + __proto__: null, + allowResizable: true, + }), growableView.buffer); + assert.strictEqual(converters.AllowSharedBufferSource(growableView, { + __proto__: null, + allowResizable: true, + }), growableView); + assert.strictEqual(converters.Uint8Array(growableView, { + __proto__: null, + allowShared: true, + allowResizable: true, + }), growableView); +} diff --git a/test/parallel/test-performance-resourcetimingbuffersize.js b/test/parallel/test-performance-resourcetimingbuffersize.js index c9c84dc9416f68..f01a31cc4403f6 100644 --- a/test/parallel/test-performance-resourcetimingbuffersize.js +++ b/test/parallel/test-performance-resourcetimingbuffersize.js @@ -37,6 +37,19 @@ async function main() { globalThis, cacheMode, ]; + + assert.throws(() => performance.setResourceTimingBufferSize(1n), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'maxSize is a BigInt and cannot be converted to a number.', + }); + + assert.throws(() => performance.setResourceTimingBufferSize(Symbol()), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'maxSize is a Symbol and cannot be converted to a number.', + }); + // Invalid buffer size values are converted to 0. const invalidValues = [ null, undefined, true, false, -1, 0.5, Infinity, NaN, '', 'foo', {}, [], () => {} ]; for (const value of invalidValues) { diff --git a/test/parallel/test-structuredClone-global.js b/test/parallel/test-structuredClone-global.js index e6b63c382b39b1..09e36ad47940ea 100644 --- a/test/parallel/test-structuredClone-global.js +++ b/test/parallel/test-structuredClone-global.js @@ -6,7 +6,7 @@ const assert = require('assert'); const prefix = "Failed to execute 'structuredClone'"; const key = 'transfer'; const context = 'Options'; -const memberConverterError = `${prefix}: ${key} in ${context} can not be converted to sequence.`; +const memberConverterError = `${prefix}: ${key} in ${context} cannot be converted to sequence.`; const dictionaryConverterError = `${prefix}: ${context} cannot be converted to a dictionary`; assert.throws(() => structuredClone(), { code: 'ERR_MISSING_ARGS' }); diff --git a/test/parallel/test-tls-alert.js b/test/parallel/test-tls-alert.js index 23c92e7293458f..64b7080e39ba25 100644 --- a/test/parallel/test-tls-alert.js +++ b/test/parallel/test-tls-alert.js @@ -48,6 +48,33 @@ const server = tls.Server({ key: loadPEM('agent2-key'), cert: loadPEM('agent2-cert') }, null).listen(0, common.mustCall(() => { + if (process.features.openssl_is_boringssl) { + let gotClientError = false; + let gotServerError = false; + function maybeClose() { + if (gotClientError && gotServerError) + server.close(); + } + + server.once('tlsClientError', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_UNSUPPORTED_PROTOCOL'); + gotServerError = true; + maybeClose(); + })); + + const client = tls.connect({ + port: server.address().port, + rejectUnauthorized: false, + secureProtocol: 'TLSv1_1_method', + }, common.mustNotCall()); + client.once('error', common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_SSL_TLSV1_ALERT_PROTOCOL_VERSION'); + gotClientError = true; + maybeClose(); + })); + return; + } + const args = ['s_client', '-quiet', '-tls1_1', '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `127.0.0.1:${server.address().port}`]; diff --git a/test/parallel/test-tls-client-auth.js b/test/parallel/test-tls-client-auth.js index 67aed40914c9fe..517054c6e290dc 100644 --- a/test/parallel/test-tls-client-auth.js +++ b/test/parallel/test-tls-client-auth.js @@ -111,7 +111,10 @@ if (tls.DEFAULT_MAX_VERSION === 'TLSv1.3') connect({ // and sends a fatal Alert to the client that the client discovers there has // been a fatal error. pair.client.conn.once('error', common.mustCall((err) => { - assert.strictEqual(err.code, 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'); + const expectedErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_TLSV1_ALERT_CERTIFICATE_REQUIRED' : + 'ERR_SSL_TLSV13_ALERT_CERTIFICATE_REQUIRED'; + assert.strictEqual(err.code, expectedErr); cleanup(); })); })); diff --git a/test/parallel/test-tls-client-getephemeralkeyinfo.js b/test/parallel/test-tls-client-getephemeralkeyinfo.js index 19728e3733d868..2107d024012c4d 100644 --- a/test/parallel/test-tls-client-getephemeralkeyinfo.js +++ b/test/parallel/test-tls-client-getephemeralkeyinfo.js @@ -2,6 +2,12 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); + +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testEphemeralKeyInfoUnsupported(); + return; +} + const fixtures = require('../common/fixtures'); const { hasOpenSSL } = require('../common/crypto'); diff --git a/test/parallel/test-tls-client-mindhsize.js b/test/parallel/test-tls-client-mindhsize.js index cd7b16ea566fe8..fa494c583a2f3b 100644 --- a/test/parallel/test-tls-client-mindhsize.js +++ b/test/parallel/test-tls-client-mindhsize.js @@ -85,21 +85,25 @@ function testDHE3072() { test(3072, false, null); } -if (hasOpenSSL(4, 0)) { - // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and - // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 - // success case is therefore replaced by a 2048 success case. - testDHE2048(true, () => test(2048, false, null, 2048)); -} else if (secLevel > 1) { - // Minimum size for OpenSSL security level 2 and above is 2048 by default - testDHE2048(true, testDHE3072); +if (!process.features.openssl_is_boringssl) { + if (hasOpenSSL(4, 0)) { + // OpenSSL 4.0 implements RFC 7919 FFDHE negotiation for TLS 1.2 and + // ignores the server-supplied dhparam in favor of FFDHE-2048. The 3072 + // success case is therefore replaced by a 2048 success case. + testDHE2048(true, () => test(2048, false, null, 2048)); + } else if (secLevel > 1) { + // Minimum size for OpenSSL security level 2 and above is 2048 by default + testDHE2048(true, testDHE3072); + } else { + testDHE1024(); + } + + assert.throws(() => test(512, true, common.mustNotCall()), + /DH parameter is less than 1024 bits/); } else { - testDHE1024(); + require('../common/boringssl').assertFiniteFieldDheUnsupported(); } -assert.throws(() => test(512, true, common.mustNotCall()), - /DH parameter is less than 1024 bits/); - for (const minDHSize of [0, -1, -Infinity, NaN]) { assert.throws(() => { tls.connect({ minDHSize }); @@ -118,7 +122,9 @@ for (const minDHSize of [true, false, null, undefined, {}, [], '', '1']) { }); } -process.on('exit', function() { - assert.strictEqual(nsuccess, 1); - assert.strictEqual(nerror, 1); -}); +if (!process.features.openssl_is_boringssl) { + process.on('exit', function() { + assert.strictEqual(nsuccess, 1); + assert.strictEqual(nerror, 1); + }); +} diff --git a/test/parallel/test-tls-client-reject.js b/test/parallel/test-tls-client-reject.js index 68922e3690eac0..cff0aabc89a774 100644 --- a/test/parallel/test-tls-client-reject.js +++ b/test/parallel/test-tls-client-reject.js @@ -30,7 +30,8 @@ const fixtures = require('../common/fixtures'); const options = { key: fixtures.readKey('rsa_private.pem'), - cert: fixtures.readKey('rsa_cert.crt') + cert: fixtures.readKey('rsa_cert.crt'), + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }; const server = tls.createServer(options, function(socket) { @@ -46,7 +47,8 @@ function unauthorized() { const socket = tls.connect({ port: server.address().port, servername: 'localhost', - rejectUnauthorized: false + rejectUnauthorized: false, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { let _data; assert(!socket.authorized); @@ -67,7 +69,8 @@ function unauthorized() { function rejectUnauthorized() { console.log('reject unauthorized'); const socket = tls.connect(server.address().port, { - servername: 'localhost' + servername: 'localhost', + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -80,7 +83,8 @@ function rejectUnauthorizedUndefined() { console.log('reject unauthorized undefined'); const socket = tls.connect(server.address().port, { servername: 'localhost', - rejectUnauthorized: undefined + rejectUnauthorized: undefined, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustNotCall()); socket.on('data', common.mustNotCall()); socket.on('error', common.mustCall(function(err) { @@ -93,7 +97,8 @@ function authorized() { console.log('connect authorized'); const socket = tls.connect(server.address().port, { ca: [fixtures.readKey('rsa_cert.crt')], - servername: 'localhost' + servername: 'localhost', + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(function() { console.log('... authorized'); assert(socket.authorized); diff --git a/test/parallel/test-tls-client-renegotiation-limit.js b/test/parallel/test-tls-client-renegotiation-limit.js index 86111d6da0b402..9b7f62865b336d 100644 --- a/test/parallel/test-tls-client-renegotiation-limit.js +++ b/test/parallel/test-tls-client-renegotiation-limit.js @@ -31,6 +31,11 @@ if (!opensslCli) { common.skip('node compiled without OpenSSL CLI.'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-dhe.js b/test/parallel/test-tls-dhe.js index 03750bc206adbe..b788d153293899 100644 --- a/test/parallel/test-tls-dhe.js +++ b/test/parallel/test-tls-dhe.js @@ -26,6 +26,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertFiniteFieldDheUnsupported(); + return; +} + const { opensslCli, hasOpenSSL, diff --git a/test/parallel/test-tls-dhparam-auto-boringssl.js b/test/parallel/test-tls-dhparam-auto-boringssl.js new file mode 100644 index 00000000000000..54f2190d1a947b --- /dev/null +++ b/test/parallel/test-tls-dhparam-auto-boringssl.js @@ -0,0 +1,19 @@ +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +if (!process.features.openssl_is_boringssl) + common.skip('only applies to BoringSSL builds'); + +const assert = require('assert'); +const tls = require('tls'); + +// BoringSSL does not provide SSL_CTX_set_dh_auto, so requesting automatic +// DH parameter selection via `dhparam: 'auto'` must throw. +assert.throws(() => { + tls.createSecureContext({ dhparam: 'auto' }); +}, { + code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', + message: 'Automatic DH parameter selection is not supported', +}); diff --git a/test/parallel/test-tls-disable-renegotiation.js b/test/parallel/test-tls-disable-renegotiation.js index f91868c6345d71..84a6ead4a5441c 100644 --- a/test/parallel/test-tls-disable-renegotiation.js +++ b/test/parallel/test-tls-disable-renegotiation.js @@ -8,6 +8,11 @@ const fixtures = require('../common/fixtures'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testRenegotiationUnsupported(); + return; +} + const tls = require('tls'); // Renegotiation as a protocol feature was dropped after TLS1.2. diff --git a/test/parallel/test-tls-ecdh-multiple.js b/test/parallel/test-tls-ecdh-multiple.js index ee52f288610956..ed60044197d7da 100644 --- a/test/parallel/test-tls-ecdh-multiple.js +++ b/test/parallel/test-tls-ecdh-multiple.js @@ -26,7 +26,7 @@ function loadPEM(n) { // OpenSSL 4.0 disables support for deprecated elliptic curves from RFC 8422 // (including secp256k1) by default. -const ecdhCurve = hasOpenSSL(4, 0) ? +const ecdhCurve = process.features.openssl_is_boringssl || hasOpenSSL(4, 0) ? 'prime256v1:secp521r1' : 'secp256k1:prime256v1:secp521r1'; @@ -67,7 +67,7 @@ const server = tls.createServer(options, (conn) => { } // Deprecated RFC 8422 curves are disabled by default in OpenSSL 4.0. - if (hasOpenSSL(4, 0)) { + if (process.features.openssl_is_boringssl || hasOpenSSL(4, 0)) { unsupportedCurves.push('secp256k1'); } diff --git a/test/parallel/test-tls-empty-sni-context.js b/test/parallel/test-tls-empty-sni-context.js index e4136ff71e1d52..6ecdfbeecbe3c9 100644 --- a/test/parallel/test-tls-empty-sni-context.js +++ b/test/parallel/test-tls-empty-sni-context.js @@ -16,7 +16,7 @@ const options = { const server = tls.createServer(options, (c) => { assert.fail('Should not be called'); }).on('tlsClientError', common.mustCall((err, c) => { - assert.match(err.message, /no suitable signature algorithm/i); + assert.match(err.message, /no suitable signature algorithm|NO_CERTIFICATE_SET/i); server.close(); })).listen(0, common.mustCall(() => { const c = tls.connect({ @@ -26,9 +26,10 @@ const server = tls.createServer(options, (c) => { }, common.mustNotCall()); c.on('error', common.mustCall((err) => { - const expectedErr = hasOpenSSL(4, 0) ? - 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? - 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; + const expectedErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_TLSV1_ALERT_INTERNAL_ERROR' : hasOpenSSL(4, 0) ? + 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? + 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; assert.strictEqual(err.code, expectedErr); })); })); diff --git a/test/parallel/test-tls-finished.js b/test/parallel/test-tls-finished.js index 8b52934b049d95..b23b4567d27ec6 100644 --- a/test/parallel/test-tls-finished.js +++ b/test/parallel/test-tls-finished.js @@ -20,7 +20,8 @@ const msg = {}; const pem = (n) => fixtures.readKey(`${n}.pem`); const server = tls.createServer({ key: pem('agent1-key'), - cert: pem('agent1-cert') + cert: pem('agent1-cert'), + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall((alice) => { msg.server = { alice: alice.getFinished(), @@ -32,7 +33,8 @@ const server = tls.createServer({ server.listen(0, common.mustCall(() => { const bob = tls.connect({ port: server.address().port, - rejectUnauthorized: false + rejectUnauthorized: false, + ...(process.features.openssl_is_boringssl ? { maxVersion: 'TLSv1.2' } : {}), }, common.mustCall(() => { msg.client = { alice: bob.getPeerFinished(), diff --git a/test/parallel/test-tls-getcipher.js b/test/parallel/test-tls-getcipher.js index 4d5042d6e6beab..2d4de5639afb70 100644 --- a/test/parallel/test-tls-getcipher.js +++ b/test/parallel/test-tls-getcipher.js @@ -36,27 +36,42 @@ const options = { honorCipherOrder: true }; +const isBoringSSL = process.features.openssl_is_boringssl; let clients = 0; +const expectedClients = isBoringSSL ? 1 : 2; const server = tls.createServer(options, common.mustCall(() => { if (--clients === 0) server.close(); -}, 2)); +}, expectedClients)); server.listen(0, '127.0.0.1', common.mustCall(function() { - clients++; - tls.connect({ - host: '127.0.0.1', - port: this.address().port, - ciphers: 'AES256-SHA256', - rejectUnauthorized: false, - maxVersion: 'TLSv1.2', - }, common.mustCall(function() { - const cipher = this.getCipher(); - assert.strictEqual(cipher.name, 'AES256-SHA256'); - assert.strictEqual(cipher.standardName, 'TLS_RSA_WITH_AES_256_CBC_SHA256'); - assert.strictEqual(cipher.version, 'TLSv1.2'); - this.end(); - })); + if (isBoringSSL) { + // BoringSSL does not provide this static RSA TLS 1.2 cipher suite on + // Node's supported cipher surface, so keep the OpenSSL getCipher() + // assertion below limited to backends that can create the context. + common.printSkipMessage('BoringSSL does not provide AES256-SHA256'); + assert.throws(() => tls.createSecureContext({ ciphers: 'AES256-SHA256' }), { + code: 'ERR_SSL_NO_CIPHER_MATCH', + library: 'SSL routines', + function: 'OPENSSL_internal', + reason: 'NO_CIPHER_MATCH', + }); + } else { + clients++; + tls.connect({ + host: '127.0.0.1', + port: this.address().port, + ciphers: 'AES256-SHA256', + rejectUnauthorized: false, + maxVersion: 'TLSv1.2', + }, common.mustCall(function() { + const cipher = this.getCipher(); + assert.strictEqual(cipher.name, 'AES256-SHA256'); + assert.strictEqual(cipher.standardName, 'TLS_RSA_WITH_AES_256_CBC_SHA256'); + assert.strictEqual(cipher.version, 'TLSv1.2'); + this.end(); + })); + } clients++; tls.connect({ @@ -70,7 +85,9 @@ server.listen(0, '127.0.0.1', common.mustCall(function() { assert.strictEqual(cipher.name, 'ECDHE-RSA-AES256-GCM-SHA384'); assert.strictEqual(cipher.standardName, 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384'); - assert.strictEqual(cipher.version, 'TLSv1.2'); + assert.strictEqual(cipher.version, isBoringSSL ? + 'TLSv1/SSLv3' : + 'TLSv1.2'); this.end(); })); })); @@ -90,9 +107,14 @@ tls.createServer({ rejectUnauthorized: false }, common.mustCall(() => { const cipher = client.getCipher(); - assert.strictEqual(cipher.name, 'TLS_AES_256_GCM_SHA384'); + const expectedCipher = isBoringSSL ? + 'TLS_AES_128_GCM_SHA256' : + 'TLS_AES_256_GCM_SHA384'; + assert.strictEqual(cipher.name, expectedCipher); assert.strictEqual(cipher.standardName, cipher.name); - assert.strictEqual(cipher.version, 'TLSv1.3'); + assert.strictEqual(cipher.version, isBoringSSL ? + 'TLSv1/SSLv3' : + 'TLSv1.3'); client.end(); })); })); diff --git a/test/parallel/test-tls-getprotocol.js b/test/parallel/test-tls-getprotocol.js index 5fe46c43c376cf..2945ff99b5a290 100644 --- a/test/parallel/test-tls-getprotocol.js +++ b/test/parallel/test-tls-getprotocol.js @@ -12,7 +12,7 @@ const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); -const clientConfigs = [ +let clientConfigs = [ { secureProtocol: 'TLSv1_method', version: 'TLSv1', @@ -27,6 +27,14 @@ const clientConfigs = [ }, ]; +if (process.features.openssl_is_boringssl) { + // Remove the TLSv1 and TLSv1.1 cases. BoringSSL does not negotiate those + // legacy protocols in this configuration; keep TLSv1.2 to cover getProtocol() + // on a successful BoringSSL TLS handshake. + common.printSkipMessage('BoringSSL: skipping TLSv1/TLSv1.1 getProtocol cases'); + clientConfigs = clientConfigs.filter(({ version }) => version === 'TLSv1.2'); +} + const serverConfig = { secureProtocol: 'TLS_method', key: fixtures.readKey('agent2-key.pem'), diff --git a/test/parallel/test-tls-handshake-error.js b/test/parallel/test-tls-handshake-error.js index 5547964780cd60..94a21a14975b5d 100644 --- a/test/parallel/test-tls-handshake-error.js +++ b/test/parallel/test-tls-handshake-error.js @@ -20,7 +20,7 @@ const server = tls.createServer({ port: this.address().port, ciphers: 'no-such-cipher' }, common.mustNotCall()); - }, /no cipher match/i); + }, /no[_ ]cipher[_ ]match/i); server.close(); })); diff --git a/test/parallel/test-tls-honorcipherorder.js b/test/parallel/test-tls-honorcipherorder.js index 5f123cd739a4c0..d86a59aa4cdc6d 100644 --- a/test/parallel/test-tls-honorcipherorder.js +++ b/test/parallel/test-tls-honorcipherorder.js @@ -16,14 +16,40 @@ const util = require('util'); // default method is updated in the future const SSL_Method = 'TLSv1_2_method'; const localhost = '127.0.0.1'; +const config = process.features.openssl_is_boringssl ? { + serverCiphers: + 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256', + clientPreferenceCiphers: + 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384', + clientPreferredCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + serverPreferredCipher: 'ECDHE-RSA-AES256-GCM-SHA384', + singleCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + defaultCipher: 'ECDHE-RSA-AES256-GCM-SHA384', + limitedDefaultCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + extraCases: [], +} : { + serverCiphers: 'AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:' + + 'ECDHE-RSA-AES128-GCM-SHA256', + clientPreferenceCiphers: 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', + clientPreferredCipher: 'AES128-GCM-SHA256', + serverPreferredCipher: 'AES256-SHA256', + singleCipher: 'AES128-SHA256', + defaultCipher: 'AES256-SHA256', + limitedDefaultCipher: 'ECDHE-RSA-AES128-GCM-SHA256', + extraCases: [ + // Server has the preference of cipher suites. AES128-GCM-SHA256 is given + // higher priority over AES128-SHA256 among client cipher suites. + [true, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'], + [undefined, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'], + ], +}; function test(honorCipherOrder, clientCipher, expectedCipher, defaultCiphers) { const soptions = { secureProtocol: SSL_Method, key: fixtures.readKey('agent2-key.pem'), cert: fixtures.readKey('agent2-cert.pem'), - ciphers: 'AES256-SHA256:AES128-GCM-SHA256:AES128-SHA256:' + - 'ECDHE-RSA-AES128-GCM-SHA256', + ciphers: config.serverCiphers, honorCipherOrder: honorCipherOrder, }; @@ -57,34 +83,27 @@ function test(honorCipherOrder, clientCipher, expectedCipher, defaultCiphers) { } // Client explicitly has the preference of cipher suites, not the default. -test(false, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES128-GCM-SHA256'); +test(false, config.clientPreferenceCiphers, config.clientPreferredCipher); -// Server has the preference of cipher suites, and AES256-SHA256 is -// the server's top choice. -test(true, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES256-SHA256'); -test(undefined, 'AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256', - 'AES256-SHA256'); - -// Server has the preference of cipher suites. AES128-GCM-SHA256 is given -// higher priority over AES128-SHA256 among client cipher suites. -test(true, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'); -test(undefined, 'AES128-SHA256:AES128-GCM-SHA256', 'AES128-GCM-SHA256'); +// Server has the preference of cipher suites. +test(true, config.clientPreferenceCiphers, config.serverPreferredCipher); +test(undefined, config.clientPreferenceCiphers, config.serverPreferredCipher); +for (const args of config.extraCases) { + test(...args); +} // As client has only one cipher, server has no choice, irrespective // of honorCipherOrder. -test(true, 'AES128-SHA256', 'AES128-SHA256'); -test(undefined, 'AES128-SHA256', 'AES128-SHA256'); +test(true, config.singleCipher, config.singleCipher); +test(undefined, config.singleCipher, config.singleCipher); -// Client did not explicitly set ciphers and client offers -// tls.DEFAULT_CIPHERS. All ciphers of the server are included in the -// default list so the negotiated cipher is selected according to the -// server's top preference of AES256-SHA256. -test(true, tls.DEFAULT_CIPHERS, 'AES256-SHA256'); -test(true, null, 'AES256-SHA256'); -test(undefined, null, 'AES256-SHA256'); +// Client did not explicitly set ciphers and client offers tls.DEFAULT_CIPHERS. +// All ciphers of the server are included in the default list so the negotiated +// cipher is selected according to server preference. +test(true, tls.DEFAULT_CIPHERS, config.defaultCipher); +test(true, null, config.defaultCipher); +test(undefined, null, config.defaultCipher); // Ensure that `tls.DEFAULT_CIPHERS` is used when its a limited cipher set. -test(true, null, 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256'); +test(true, null, config.limitedDefaultCipher, config.limitedDefaultCipher); diff --git a/test/parallel/test-tls-junk-server.js b/test/parallel/test-tls-junk-server.js index 42f089f8f90ed2..b6ff3cd2a467f2 100644 --- a/test/parallel/test-tls-junk-server.js +++ b/test/parallel/test-tls-junk-server.js @@ -24,7 +24,7 @@ server.listen(0, common.mustCall(function() { // Different OpenSSL versions report different errors for junk data on a // TLS connection, depending on which record validation check fires first. const expectedErrorMessage = - /wrong version number|packet length too long|bad record type/; + /wrong[ _]version[ _]number|packet length too long|bad record type/i; req.once('error', common.mustCall(function(err) { assert.match(err.message, expectedErrorMessage); server.close(); diff --git a/test/parallel/test-tls-key-mismatch.js b/test/parallel/test-tls-key-mismatch.js index df8848a03de4a9..797c7c171dc5ff 100644 --- a/test/parallel/test-tls-key-mismatch.js +++ b/test/parallel/test-tls-key-mismatch.js @@ -31,9 +31,11 @@ const { hasOpenSSL3 } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); -const errorMessageRegex = hasOpenSSL3 ? - /^Error: error:05800074:x509 certificate routines::key values mismatch$/ : - /^Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch$/; +const errorMessageRegex = process.features.openssl_is_boringssl ? + /^Error: error:0b000074:X\.509 certificate routines:OPENSSL_internal:KEY_VALUES_MISMATCH$/ : + hasOpenSSL3 ? + /^Error: error:05800074:x509 certificate routines::key values mismatch$/ : + /^Error: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch$/; const options = { key: fixtures.readKey('agent1-key.pem'), diff --git a/test/parallel/test-tls-max-send-fragment.js b/test/parallel/test-tls-max-send-fragment.js index 009021045624bb..2e319fcdaeafea 100644 --- a/test/parallel/test-tls-max-send-fragment.js +++ b/test/parallel/test-tls-max-send-fragment.js @@ -60,9 +60,15 @@ const server = tls.createServer({ assert.throws(() => c.setMaxSendFragment(Symbol()), { name: 'TypeError' }); - // Lower and upper limits. - assert(!c.setMaxSendFragment(511)); - assert(!c.setMaxSendFragment(16385)); + // OpenSSL enforces Node's documented fragment size range. BoringSSL accepts + // both out-of-range values and reports success, so assert that difference + // explicitly instead of using a truthiness shortcut. + const acceptsOutOfRangeFragmentSize = + process.features.openssl_is_boringssl; + assert.strictEqual(c.setMaxSendFragment(511), + acceptsOutOfRangeFragmentSize); + assert.strictEqual(c.setMaxSendFragment(16385), + acceptsOutOfRangeFragmentSize); // Correct fragment size. assert(c.setMaxSendFragment(maxChunk)); diff --git a/test/parallel/test-tls-min-max-version.js b/test/parallel/test-tls-min-max-version.js index 4903d92f5c5700..abddbbeb0eba1b 100644 --- a/test/parallel/test-tls-min-max-version.js +++ b/test/parallel/test-tls-min-max-version.js @@ -4,6 +4,12 @@ const common = require('../common'); if (!common.hasCrypto) { common.skip('missing crypto'); } + +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testLegacyProtocolUnsupported(); + return; +} + const { hasOpenSSL, hasOpenSSL3, diff --git a/test/parallel/test-tls-multi-key.js b/test/parallel/test-tls-multi-key.js index 89f9931e5bdd77..0a9c6f108bf675 100644 --- a/test/parallel/test-tls-multi-key.js +++ b/test/parallel/test-tls-multi-key.js @@ -27,6 +27,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertMultiKeyUnsupported(); + return; +} + const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); diff --git a/test/parallel/test-tls-multi-pfx.js b/test/parallel/test-tls-multi-pfx.js index 526b77b1484cd3..fec697cd3b7093 100644 --- a/test/parallel/test-tls-multi-pfx.js +++ b/test/parallel/test-tls-multi-pfx.js @@ -3,6 +3,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testMultiPfxSelectionDifference(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-no-cert-required.js b/test/parallel/test-tls-no-cert-required.js index b3dcfa516ab502..499ab2dfd14ed2 100644 --- a/test/parallel/test-tls-no-cert-required.js +++ b/test/parallel/test-tls-no-cert-required.js @@ -28,10 +28,15 @@ const assert = require('assert'); const tls = require('tls'); // Omitting the cert or pfx option to tls.createServer() should not throw. -// AECDH-NULL-SHA is a no-authentication/no-encryption cipher and hence -// doesn't need a certificate. -tls.createServer({ ciphers: 'AECDH-NULL-SHA' }) - .listen(0, common.mustCall(close)); +if (process.features.openssl_is_boringssl) { + // AECDH-NULL-SHA is a no-authentication/no-encryption cipher and hence + // does not need a certificate. BoringSSL does not provide that anonymous + // cipher suite, so only this cipher-specific no-cert case is skipped. + common.printSkipMessage('BoringSSL: skipping anonymous AECDH-NULL-SHA case'); +} else { + tls.createServer({ ciphers: 'AECDH-NULL-SHA' }) + .listen(0, common.mustCall(close)); +} tls.createServer(assert.fail) .listen(0, common.mustCall(close)); diff --git a/test/parallel/test-tls-options-boolean-check.js b/test/parallel/test-tls-options-boolean-check.js index 900a39f0c1cd42..f7dd7bb102f361 100644 --- a/test/parallel/test-tls-options-boolean-check.js +++ b/test/parallel/test-tls-options-boolean-check.js @@ -40,9 +40,23 @@ const keyDataView = toDataView(keyBuff); const certDataView = toDataView(certBuff); const caArrDataView = toDataView(caCert); +function filterBoringSSLKeyCertArrayCases(options, setName) { + if (!process.features.openssl_is_boringssl) + return options; + + // The array-valued cases exercise multi-identity key/cert handling. + // BoringSSL may reject those cases with backend key/cert mismatch errors + // before the boolean/type validation this test is targeting. Keep the scalar + // cases so tls.createServer() option type validation is still covered. + common.printSkipMessage( + `BoringSSL: skipping ${setName} key/cert array cases`); + return options.filter(([key, cert]) => !Array.isArray(key) && + !Array.isArray(cert)); +} + // Checks to ensure tls.createServer doesn't throw an error // Format ['key', 'cert'] -[ +const validOptions = [ [keyBuff, certBuff], [false, certBuff], [keyBuff, false], @@ -62,13 +76,16 @@ const caArrDataView = toDataView(caCert); [false, [certStr, certStr2]], [[{ pem: keyBuff }], false], [[{ pem: keyBuff }, { pem: keyBuff }], false], -].forEach(([key, cert]) => { - tls.createServer({ key, cert }); -}); +]; + +filterBoringSSLKeyCertArrayCases(validOptions, 'valid') + .forEach(([key, cert]) => { + tls.createServer({ key, cert }); + }); // Checks to ensure tls.createServer predictably throws an error // Format ['key', 'cert', 'expected message'] -[ +const invalidKeyOptions = [ [true, certBuff], [true, certStr], [true, certArrBuff], @@ -80,7 +97,10 @@ const caArrDataView = toDataView(caCert); [[true, keyStr2], [certStr, certStr2], 0], [[true, false], [certBuff, certBuff2], 0], [true, [certBuff, certBuff2]], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidKeyOptions, 'invalid key')) { const val = index === undefined ? key : key[index]; assert.throws(() => { tls.createServer({ key, cert }); @@ -91,9 +111,9 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} -[ +const invalidCertOptions = [ [keyBuff, true], [keyStr, true], [keyArrBuff, true], @@ -106,7 +126,10 @@ const caArrDataView = toDataView(caCert); [[keyStr, keyStr2], [certStr, true], 1], [[keyStr, keyStr2], [true, false], 0], [[keyStr, keyStr2], true], -].forEach(([key, cert, index]) => { +]; + +for (const [key, cert, index] of + filterBoringSSLKeyCertArrayCases(invalidCertOptions, 'invalid cert')) { const val = index === undefined ? cert : cert[index]; assert.throws(() => { tls.createServer({ key, cert }); @@ -117,7 +140,7 @@ const caArrDataView = toDataView(caCert); 'instance of Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(val) }); -}); +} // Checks to ensure tls.createServer works with the CA parameter // Format ['key', 'cert', 'ca'] diff --git a/test/parallel/test-tls-passphrase.js b/test/parallel/test-tls-passphrase.js index 8d802400f6ee3b..4372da249bb509 100644 --- a/test/parallel/test-tls-passphrase.js +++ b/test/parallel/test-tls-passphrase.js @@ -223,7 +223,7 @@ server.listen(0, common.mustCall(function() { }, onSecureConnect()); })).unref(); -const errMessageDecrypt = /bad decrypt/; +const errMessageDecrypt = /bad[ _]decrypt/i; // Missing passphrase assert.throws(function() { diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js index 3d355c344f2f5f..ce3b7522a26dc8 100644 --- a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js @@ -14,6 +14,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const assert = require('assert'); const { describe, it } = require('node:test'); const tls = require('tls'); diff --git a/test/parallel/test-tls-psk-circuit.js b/test/parallel/test-tls-psk-circuit.js index bdf9c86c26a7b6..c9c93d53350165 100644 --- a/test/parallel/test-tls-psk-circuit.js +++ b/test/parallel/test-tls-psk-circuit.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { hasOpenSSL } = require('../common/crypto'); const assert = require('assert'); const tls = require('tls'); diff --git a/test/parallel/test-tls-psk-server.js b/test/parallel/test-tls-psk-server.js index af038493469880..692550fc1c198b 100644 --- a/test/parallel/test-tls-psk-server.js +++ b/test/parallel/test-tls-psk-server.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { opensslCli } = require('../common/crypto'); if (!opensslCli) { diff --git a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js index 9f4458e0a7d671..cca22067a0fe19 100644 --- a/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js +++ b/test/parallel/test-tls-reduced-SECLEVEL-in-cipher.js @@ -4,6 +4,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').assertOpenSSLSecurityLevelsUnsupported(); + return; +} + const assert = require('assert'); const tls = require('tls'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js b/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js index 2fb43b9cbbf87a..9c30989af0afb3 100644 --- a/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js +++ b/test/parallel/test-tls-server-failed-handshake-emits-clienterror.js @@ -22,7 +22,7 @@ const server = tls.createServer({}) 'Instance of Error should be passed to error handler'); assert.match( e.message, - /SSL routines:[^:]*:wrong version number/, + /SSL routines:[^:]*:wrong[ _]version[ _]number/i, ); server.close(); diff --git a/test/parallel/test-tls-server-verify.js b/test/parallel/test-tls-server-verify.js index 94f372d37a3b1f..439e321310305a 100644 --- a/test/parallel/test-tls-server-verify.js +++ b/test/parallel/test-tls-server-verify.js @@ -47,7 +47,7 @@ const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = const tls = require('tls'); const fixtures = require('../common/fixtures'); -const testCases = +let testCases = [{ title: 'Do not request certs. Everyone is unauthorized.', requestCert: false, rejectUnauthorized: false, @@ -125,6 +125,15 @@ const testCases = ] }, ]; +if (process.features.openssl_is_boringssl) { + // Remove the delayed client-certificate verification case. It depends on TLS + // renegotiation to request a client certificate after the initial handshake, + // but BoringSSL does not support caller-initiated renegotiation. + common.printSkipMessage( + 'BoringSSL: skipping renegotiated client certificate verification case'); + testCases = testCases.filter((tcase) => !tcase.renegotiate); +} + function filenamePEM(n) { return fixtures.path('keys', `${n}.pem`); } diff --git a/test/parallel/test-tls-session-cache.js b/test/parallel/test-tls-session-cache.js index aaf9c2c03c83e9..ae560e567980c9 100644 --- a/test/parallel/test-tls-session-cache.js +++ b/test/parallel/test-tls-session-cache.js @@ -37,6 +37,7 @@ const fixtures = require('../common/fixtures'); const assert = require('assert'); const tls = require('tls'); const { spawn } = require('child_process'); +const isBoringSSL = process.features.openssl_is_boringssl; doTest({ tickets: false }, function() { doTest({ tickets: true }, function() { @@ -56,7 +57,9 @@ function doTest(testOptions, callback) { requestCert: true, rejectUnauthorized: false, secureProtocol: 'TLS_method', - ciphers: 'RSA@SECLEVEL=0' + // BoringSSL supports the RSA cipher selector, but not OpenSSL's + // cipher-string policy command syntax. + ciphers: isBoringSSL ? 'RSA' : 'RSA@SECLEVEL=0' }; let requestCount = 0; let resumeCount = 0; @@ -105,7 +108,7 @@ function doTest(testOptions, callback) { server.listen(0, common.mustCall(function() { const args = [ 's_client', - '-tls1', + isBoringSSL ? '-tls1_2' : '-tls1', '-cipher', (hasOpenSSL(3, 1) ? 'DEFAULT:@SECLEVEL=0' : 'DEFAULT'), '-connect', `localhost:${this.address().port}`, '-servername', 'ohgod', diff --git a/test/parallel/test-tls-set-ciphers-error.js b/test/parallel/test-tls-set-ciphers-error.js index 3cfc8c391bf7d5..b79bd512ffe1db 100644 --- a/test/parallel/test-tls-set-ciphers-error.js +++ b/test/parallel/test-tls-set-ciphers-error.js @@ -21,8 +21,12 @@ const { hasOpenSSL } = require('../common/crypto'); assert.throws(() => tls.createServer(options, common.mustNotCall()), /no[_ ]cipher[_ ]match/i); options.ciphers = 'TLS_not_a_cipher'; - assert.throws(() => tls.createServer(options, common.mustNotCall()), - /no[_ ]cipher[_ ]match/i); + if (process.features.openssl_is_boringssl) { + tls.createServer(options).close(); + } else { + assert.throws(() => tls.createServer(options, common.mustNotCall()), + /no[_ ]cipher[_ ]match/i); + } } // Cipher name matching is case-sensitive prior to OpenSSL 4.0, and diff --git a/test/parallel/test-tls-set-default-ca-certificates-recovery.js b/test/parallel/test-tls-set-default-ca-certificates-recovery.js index e3eb0e84149ae8..ea6f98d5686e03 100644 --- a/test/parallel/test-tls-set-default-ca-certificates-recovery.js +++ b/test/parallel/test-tls-set-default-ca-certificates-recovery.js @@ -27,7 +27,9 @@ function testRecovery(expectedCerts) { { const invalidCert = '-----BEGIN CERTIFICATE-----\nvalid cert content\n-----END CERTIFICATE-----'; assert.throws(() => tls.setDefaultCACertificates([fixtureCert, invalidCert]), { - code: 'ERR_OSSL_PEM_ASN1_LIB', + code: process.features.openssl_is_boringssl ? + 'ERR_OSSL_PEM_ASN.1_ENCODING_ROUTINES' : + 'ERR_OSSL_PEM_ASN1_LIB', }); assertEqualCerts(tls.getCACertificates('default'), expectedCerts); } diff --git a/test/parallel/test-tls-set-sigalgs.js b/test/parallel/test-tls-set-sigalgs.js index 1bce814f3e8604..e1bf8b93f8a342 100644 --- a/test/parallel/test-tls-set-sigalgs.js +++ b/test/parallel/test-tls-set-sigalgs.js @@ -39,9 +39,14 @@ function test(csigalgs, ssigalgs, shared_sigalgs, cerr, serr) { assert.ifError(pair.client.err); assert(pair.server.conn); assert(pair.client.conn); + // BoringSSL's OpenSSL-compatible SSL_get_shared_sigalgs() API always + // returns zero, so a successful handshake still reports an empty list. + const expectedSharedSigalgs = process.features.openssl_is_boringssl ? + [] : + shared_sigalgs; assert.deepStrictEqual( pair.server.conn.getSharedSigalgs(), - shared_sigalgs + expectedSharedSigalgs ); } else { if (serr) { @@ -69,10 +74,13 @@ test('RSA-PSS+SHA256:RSA-PSS+SHA512:ECDSA+SHA256', const handshakeErr = hasOpenSSL(4, 0) ? 'ERR_SSL_TLS_ALERT_HANDSHAKE_FAILURE' : hasOpenSSL(3, 2) ? 'ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE' : 'ERR_SSL_SSLV3_ALERT_HANDSHAKE_FAILURE'; +const noSharedSigalgsErr = process.features.openssl_is_boringssl ? + 'ERR_SSL_NO_COMMON_SIGNATURE_ALGORITHMS' : + 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'; test('RSA-PSS+SHA384', 'ECDSA+SHA256', undefined, handshakeErr, - 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); + noSharedSigalgsErr); test('RSA-PSS+SHA384:ECDSA+SHA256', 'ECDSA+SHA384:RSA-PSS+SHA256', undefined, handshakeErr, - 'ERR_SSL_NO_SHARED_SIGNATURE_ALGORITHMS'); + noSharedSigalgsErr); diff --git a/test/parallel/test-tls-socket-failed-handshake-emits-error.js b/test/parallel/test-tls-socket-failed-handshake-emits-error.js index c88f0c3a1855f2..c64d4ad4aabe8d 100644 --- a/test/parallel/test-tls-socket-failed-handshake-emits-error.js +++ b/test/parallel/test-tls-socket-failed-handshake-emits-error.js @@ -22,7 +22,7 @@ const server = net.createServer(common.mustCall((c) => { 'Instance of Error should be passed to error handler'); assert.match( e.message, - /SSL routines:[^:]*:wrong version number/, + /SSL routines:[^:]*:wrong[ _]version[ _]number/i, ); })); diff --git a/test/parallel/test-tls-ticket-cluster.js b/test/parallel/test-tls-ticket-cluster.js index 2ed4abb93c8d47..f183b53f24c0b9 100644 --- a/test/parallel/test-tls-ticket-cluster.js +++ b/test/parallel/test-tls-ticket-cluster.js @@ -24,6 +24,11 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const assert = require('assert'); const tls = require('tls'); const cluster = require('cluster'); diff --git a/test/parallel/test-tls-ticket.js b/test/parallel/test-tls-ticket.js index 0a77e52fb275cd..8316f5e8da8d8f 100644 --- a/test/parallel/test-tls-ticket.js +++ b/test/parallel/test-tls-ticket.js @@ -30,6 +30,12 @@ const net = require('net'); const crypto = require('crypto'); const fixtures = require('../common/fixtures'); +if (process.features.openssl_is_boringssl && + tls.DEFAULT_MAX_VERSION !== 'TLSv1.2') { + require('../common/boringssl').testTls13SessionTicketSemanticsDiffer(); + return; +} + const keys = crypto.randomBytes(48); const serverLog = []; const ticketLog = []; diff --git a/test/parallel/test-webapi-sharedarraybuffer-rejection.js b/test/parallel/test-webapi-sharedarraybuffer-rejection.js index c5503dfc0a1b2d..c0450e8a6ce6e1 100644 --- a/test/parallel/test-webapi-sharedarraybuffer-rejection.js +++ b/test/parallel/test-webapi-sharedarraybuffer-rejection.js @@ -133,33 +133,28 @@ test('webidl converters.BufferSource accepts regular TypedArray', () => { assert.strictEqual(converters.BufferSource(ta), ta); }); -test('webidl converters.ArrayBufferView rejects SAB-backed Uint8Array', () => { +test('webidl converters.Uint8Array rejects SAB-backed Uint8Array', () => { assert.throws( - () => converters.ArrayBufferView(sabView), + () => converters.Uint8Array(sabView), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView rejects SAB-backed DataView', () => { +test('webidl converters.Uint8Array rejects DataView', () => { assert.throws( - () => converters.ArrayBufferView(sabDataView), + () => converters.Uint8Array(sabDataView), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView rejects non-view', () => { +test('webidl converters.Uint8Array rejects non-view', () => { assert.throws( - () => converters.ArrayBufferView('not a view'), + () => converters.Uint8Array('not a view'), { code: 'ERR_INVALID_ARG_TYPE' }, ); }); -test('webidl converters.ArrayBufferView accepts regular Uint8Array', () => { +test('webidl converters.Uint8Array accepts regular Uint8Array', () => { const ta = new Uint8Array(4); - assert.strictEqual(converters.ArrayBufferView(ta), ta); -}); - -test('webidl converters.ArrayBufferView accepts regular DataView', () => { - const dv = new DataView(new ArrayBuffer(4)); - assert.strictEqual(converters.ArrayBufferView(dv), dv); + assert.strictEqual(converters.Uint8Array(ta), ta); }); diff --git a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js index a96e709095430f..316d706e7b7948 100644 --- a/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js +++ b/test/parallel/test-webcrypto-aead-decrypt-detached-buffer.js @@ -29,14 +29,11 @@ async function test(algorithmName, keyLength, ivLength, format = 'raw') { const tests = [ test('AES-GCM', 32, 12), + test('ChaCha20-Poly1305', 32, 12, 'raw-secret'), ]; if (hasOpenSSL(3)) { tests.push(test('AES-OCB', 32, 12, 'raw-secret')); } -if (!process.features.openssl_is_boringssl) { - tests.push(test('ChaCha20-Poly1305', 32, 12, 'raw-secret')); -} - Promise.all(tests).then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-crypto-job-mode.js b/test/parallel/test-webcrypto-crypto-job-mode.js new file mode 100644 index 00000000000000..327c6a6f154cc4 --- /dev/null +++ b/test/parallel/test-webcrypto-crypto-job-mode.js @@ -0,0 +1,228 @@ +// Flags: --expose-internals +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); +const { types: { isCryptoKey } } = require('util'); +const { internalBinding } = require('internal/test/binding'); +const { + getCryptoKeyHandle, +} = require('internal/crypto/keys'); +const { + getUsagesMask, +} = require('internal/crypto/util'); +const { + aesCipher, +} = require('internal/crypto/aes'); + +const { + AESCipherJob, + EcKeyPairGenJob, + HashJob, + SecretKeyGenJob, + kCryptoJobWebCrypto, + kKeyVariantAES_CBC_128, + kWebCryptoCipherEncrypt, +} = internalBinding('crypto'); + +const { subtle } = globalThis.crypto; + +// Defines Object.prototype setters that fail the test if native result objects +// carrying key or shared secret material use [[Set]]. +async function withObjectPrototypeSetters(names, fn) { + const descriptors = new Map(); + for (const name of names) { + descriptors.set(name, Object.getOwnPropertyDescriptor(Object.prototype, name)); + Object.defineProperty(Object.prototype, name, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`Object.prototype.${name} getter`), + set: common.mustNotCall(`Object.prototype.${name} setter`), + }); + } + + try { + return await fn(); + } finally { + for (const name of names) { + const descriptor = descriptors.get(name); + if (descriptor === undefined) { + delete Object.prototype[name]; + } else { + Object.defineProperty(Object.prototype, name, descriptor); + } + } + } +} + +(async function() { + { + const promise = new HashJob( + kCryptoJobWebCrypto, + 'sha256', + Buffer.from('hello'), + undefined).run(); + + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + + let settled = false; + promise.then(common.mustCall(() => { settled = true; })); + await Promise.resolve(); + assert.strictEqual(settled, false); + + const digest = await promise; + assert(digest instanceof ArrayBuffer); + assert.strictEqual(digest.byteLength, 32); + assert.strictEqual(Object.hasOwn(digest, 'then'), false); + } + + { + const key = await new SecretKeyGenJob( + kCryptoJobWebCrypto, + 128, + { name: 'AES-CBC', length: 128 }, + getUsagesMask(new Set(['encrypt'])), + true).run(); + + assert(isCryptoKey(key)); + assert(key instanceof CryptoKey); + assert.strictEqual(key.type, 'secret'); + assert.strictEqual(key.extractable, true); + assert.deepStrictEqual(key.usages, ['encrypt']); + } + + { + const pair = await withObjectPrototypeSetters( + ['publicKey', 'privateKey'], + () => new EcKeyPairGenJob( + kCryptoJobWebCrypto, + 'P-256', + undefined, + { name: 'ECDSA', namedCurve: 'P-256' }, + getUsagesMask(new Set(['verify'])), + getUsagesMask(new Set(['sign'])), + true).run()); + + assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype); + assert.strictEqual(Object.hasOwn(pair, 'then'), false); + assert(isCryptoKey(pair.publicKey)); + assert(isCryptoKey(pair.privateKey)); + assert(pair.publicKey instanceof CryptoKey); + assert(pair.privateKey instanceof CryptoKey); + assert.strictEqual(pair.publicKey.type, 'public'); + assert.strictEqual(pair.privateKey.type, 'private'); + assert.deepStrictEqual(pair.publicKey.usages, ['verify']); + assert.deepStrictEqual(pair.privateKey.usages, ['sign']); + } + + { + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + false, + ['encrypt']); + assert.throws( + () => new AESCipherJob( + kCryptoJobWebCrypto, + kWebCryptoCipherEncrypt, + getCryptoKeyHandle(key), + Buffer.alloc(16), + kKeyVariantAES_CBC_128, + Buffer.alloc(15)), + /Invalid initialization vector/); + + const promise = aesCipher( + kWebCryptoCipherEncrypt, + key, + Buffer.alloc(16), + { name: 'AES-CBC', iv: Buffer.alloc(15) }); + + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + await assert.rejects(promise, (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual( + err.message, + 'The operation failed for an operation-specific reason'); + assert(err.cause); + assert.match(err.cause.message, /Invalid initialization vector/); + return true; + }); + } + + { + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + false, + ['encrypt', 'decrypt']); + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = new Uint8Array(await subtle.encrypt( + { name: 'AES-CBC', iv }, + key, + Buffer.alloc(16))); + ciphertext[0] ^= 0xff; + + await assert.rejects( + subtle.decrypt({ name: 'AES-CBC', iv }, key, ciphertext), + (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual( + err.message, + 'The operation failed for an operation-specific reason'); + assert(err.cause); + assert.strictEqual(typeof err.cause.message, 'string'); + assert.notStrictEqual(err.cause.message, ''); + return true; + }); + } + + { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify']); + const data = Buffer.from('hello'); + const signature = await subtle.sign('HMAC', key, data); + assert(signature instanceof ArrayBuffer); + assert.strictEqual( + typeof await subtle.verify('HMAC', key, signature, data), + 'boolean'); + } + + { + Object.defineProperty(CryptoKey.prototype, 'then', { + __proto__: null, + configurable: true, + get: common.mustNotCall('CryptoKey.prototype.then getter'), + }); + + try { + const key = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt']); + assert(isCryptoKey(key)); + assert.strictEqual(Object.hasOwn(key, 'then'), false); + } finally { + delete CryptoKey.prototype.then; + } + } + + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { + const pair = await subtle.generateKey( + { name: 'ML-KEM-768' }, + true, + ['encapsulateBits', 'decapsulateBits']); + const result = await withObjectPrototypeSetters( + ['sharedKey', 'ciphertext'], + () => subtle.encapsulateBits({ name: 'ML-KEM-768' }, pair.publicKey)); + + assert.strictEqual(Object.getPrototypeOf(result), Object.prototype); + assert.strictEqual(Object.hasOwn(result, 'then'), false); + assert(result.sharedKey instanceof ArrayBuffer); + assert(result.ciphertext instanceof ArrayBuffer); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-brand-check.js b/test/parallel/test-webcrypto-cryptokey-brand-check.js new file mode 100644 index 00000000000000..3fe8aaa181a226 --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-brand-check.js @@ -0,0 +1,132 @@ +'use strict'; + +// The four CryptoKey prototype getters (`type`, `extractable`, +// `algorithm`, `usages`) are user-configurable per Web IDL, so they +// can be invoked with an arbitrary `this`. The native callbacks that +// implement them must brand-check their receiver and throw cleanly +// (ERR_INVALID_THIS) rather than crashing the process or returning +// garbage. This test exercises four progressively more hostile +// receiver shapes, including subverting `instanceof` via +// `Symbol.hasInstance`, to make sure the C++ brand check holds. +// +// It also verifies that `util.types.isCryptoKey()` cannot be fooled +// by prototype spoofing. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { types: { isCryptoKey } } = require('node:util'); +const { subtle } = globalThis.crypto; + +(async () => { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign'], + ); + + const CryptoKey = key.constructor; + + // Capture the underlying prototype getters once, so that subsequent + // tampering with `CryptoKey.prototype` cannot affect what we call. + const getters = { + type: Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'type').get, + extractable: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'extractable').get, + algorithm: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'algorithm').get, + usages: + Object.getOwnPropertyDescriptor(CryptoKey.prototype, 'usages').get, + }; + + // Sanity: each getter works on a real CryptoKey. + Object.entries(getters).forEach(([name, getter]) => { + assert.notStrictEqual(getter.call(key), undefined, `baseline ${name}`); + }); + assert.strictEqual(isCryptoKey(key), true); + assert.strictEqual(Object.hasOwn(CryptoKey, 'getSlots'), false); + const internalProto = Object.getPrototypeOf(key); + assert.strictEqual(Object.hasOwn(internalProto, 'getSlots'), false); + assert.strictEqual('getSlots' in internalProto, false); + assert.strictEqual(internalProto.constructor, CryptoKey); + assert.strictEqual(Object.getPrototypeOf(internalProto), CryptoKey.prototype); + + const invalidThis = { code: 'ERR_INVALID_THIS', name: 'TypeError' }; + + // Plain object receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call({}), invalidThis); + }); + + // Null-prototype object receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call({ __proto__: null }), invalidThis); + }); + + // Primitive receiver. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(1), invalidThis); + }); + + // Null. + Object.entries(getters).forEach(([, getter]) => { + // eslint-disable-next-line no-useless-call + assert.throws(() => getter.call(null), invalidThis); + }); + + // Undefined. + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(), invalidThis); + }); + + // Function + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(function() {}), invalidThis); + }); + + // Prototype spoofing with InternalCryptoKey.prototype must not pass + // util.types.isCryptoKey(). + const spoofed = {}; + Object.setPrototypeOf(spoofed, Object.getPrototypeOf(key)); + assert.strictEqual(spoofed instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(spoofed), false); + await assert.rejects( + subtle.sign('HMAC', spoofed, Buffer.from('payload')), + invalidThis); + await assert.rejects( + subtle.exportKey('jwk', spoofed), + invalidThis); + + // Subvert `instanceof CryptoKey` via Symbol.hasInstance, then + // invoke the native getters on a forged object. The C++ tag + // check must reject the receiver even though `instanceof` + // reports true. + Object.defineProperty(CryptoKey, Symbol.hasInstance, { + configurable: true, + value: () => true, + }); + const fake = { foo: 'bar' }; + assert.strictEqual(fake instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(fake), false); + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(fake), invalidThis); + }); + + // Subverted `instanceof` plus a real BaseObject of a different + // kind (a Buffer) as the receiver. Without the C++ tag check + // this would type-confuse `Unwrap`. + const buf = Buffer.alloc(16); + assert.strictEqual(buf instanceof CryptoKey, true); + assert.strictEqual(isCryptoKey(buf), false); + Object.entries(getters).forEach(([, getter]) => { + assert.throws(() => getter.call(buf), invalidThis); + }); + + // The real CryptoKey continues to work after all of the above. + assert.strictEqual(getters.type.call(key), 'secret'); + assert.strictEqual(getters.extractable.call(key), true); + assert.strictEqual(getters.algorithm.call(key).name, 'HMAC'); + assert.deepStrictEqual(getters.usages.call(key), ['sign']); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-clone-transfer.js b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js new file mode 100644 index 00000000000000..4983e1c0bda308 --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-clone-transfer.js @@ -0,0 +1,351 @@ +'use strict'; + +// Tests that CryptoKey instances can be structured-cloned (same-realm +// via `structuredClone`, cross-realm via `MessagePort.postMessage` and +// `Worker.postMessage`) and that the clones: +// 1. preserve all of [[type]], [[extractable]], [[algorithm]], +// [[usages]] internal slots (as observed through both the public +// accessors and the custom util.inspect output), +// 2. are usable in cryptographic operations (sign/verify/encrypt/ +// decrypt/exportKey) and produce the same output as the original, +// 3. can themselves be cloned again (round-trip), and +// 4. work for secret, public, and private keys and for both +// extractable and non-extractable keys. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { inspect } = require('node:util'); +const { once } = require('node:events'); +const { Worker, MessageChannel } = require('node:worker_threads'); +const { subtle } = globalThis.crypto; + +function assertNoOwnReflection(key) { + assert.deepStrictEqual(Object.getOwnPropertySymbols(key), []); + assert.deepStrictEqual(Object.getOwnPropertyNames(key), []); + assert.deepStrictEqual(Reflect.ownKeys(key), []); +} + +function describeKey(key) { + const algorithm = { ...key.algorithm }; + if (algorithm.hash !== undefined) + algorithm.hash = { ...algorithm.hash }; + if (algorithm.publicExponent !== undefined) + algorithm.publicExponent = Array.from(algorithm.publicExponent); + return { + type: key.type, + extractable: key.extractable, + algorithm, + usages: [...key.usages].sort(), + }; +} + +function assertSameCryptoKey(a, b) { + assert.notStrictEqual(a, b); + assert.strictEqual(a.type, b.type); + assert.strictEqual(a.extractable, b.extractable); + assert.deepStrictEqual(a.algorithm, b.algorithm); + assert.deepStrictEqual([...a.usages].sort(), [...b.usages].sort()); + assertNoOwnReflection(a); + assertNoOwnReflection(b); + // util.inspect reads native internal slots directly, so a clone's + // rendered form must match the original's. + assert.strictEqual(inspect(a, { depth: 4 }), inspect(b, { depth: 4 })); + // assert.deepStrictEqual on CryptoKey objects goes through the + // dedicated isCryptoKey branch in comparisons.js; a clone must be + // deep-equal to its source. + assert.deepStrictEqual(a, b); +} + +async function roundTripViaMessageChannel(key) { + const { port1, port2 } = new MessageChannel(); + port1.postMessage(key); + const [received] = await once(port2, 'message'); + port1.close(); + port2.close(); + return received; +} + +async function checkHmacKey(original) { + const data = Buffer.from('some data to sign'); + + const cloned = structuredClone(original); + assertSameCryptoKey(original, cloned); + + const viaPort = await roundTripViaMessageChannel(original); + assertSameCryptoKey(original, viaPort); + + // Round-trip: clone a clone. + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(original, clonedAgain); + const viaPortAgain = await roundTripViaMessageChannel(cloned); + assertSameCryptoKey(original, viaPortAgain); + + // Signatures produced by every copy must match. + const sigs = await Promise.all( + [original, cloned, viaPort, clonedAgain, viaPortAgain].map( + (k) => subtle.sign('HMAC', k, data), + ), + ); + for (let i = 1; i < sigs.length; i++) { + assert.deepStrictEqual(Buffer.from(sigs[0]), Buffer.from(sigs[i])); + } + + // Each copy must verify a signature produced by any other copy. + for (const verifier of [original, cloned, viaPort, clonedAgain]) { + for (const sig of sigs) { + assert.strictEqual( + await subtle.verify('HMAC', verifier, sig, data), true); + } + } + + // Exported JWK must match byte-for-byte when extractable. + if (original.extractable) { + const jwks = await Promise.all( + [original, cloned, viaPort, clonedAgain].map( + (k) => subtle.exportKey('jwk', k), + ), + ); + for (let i = 1; i < jwks.length; i++) { + assert.deepStrictEqual(jwks[0], jwks[i]); + } + } else { + // Non-extractable keys must refuse export on every copy. + for (const k of [cloned, viaPort, clonedAgain]) { + await assert.rejects(subtle.exportKey('jwk', k), + { name: 'InvalidAccessError' }); + } + } +} + +async function checkAsymmetricKeyPair({ publicKey, privateKey }) { + const data = Buffer.from('payload'); + + for (const original of [publicKey, privateKey]) { + const cloned = structuredClone(original); + assertSameCryptoKey(original, cloned); + const viaPort = await roundTripViaMessageChannel(original); + assertSameCryptoKey(original, viaPort); + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(original, clonedAgain); + } + + // Sign with the original private key, verify with every cloned public key. + const signature = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, privateKey, data); + const publicClones = [ + publicKey, + structuredClone(publicKey), + await roundTripViaMessageChannel(publicKey), + structuredClone(await roundTripViaMessageChannel(publicKey)), + ]; + for (const pub of publicClones) { + assert.strictEqual( + await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, + pub, signature, data), + true); + } + + // Sign with every cloned private key, verify with the original public key. + const privateClones = [ + structuredClone(privateKey), + await roundTripViaMessageChannel(privateKey), + structuredClone(await roundTripViaMessageChannel(privateKey)), + ]; + for (const priv of privateClones) { + const sig = await subtle.sign( + { name: 'ECDSA', hash: 'SHA-256' }, priv, data); + assert.strictEqual( + await subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, + publicKey, sig, data), + true); + } +} + +async function checkTransferToWorker(key) { + // A one-shot worker that receives a key, asserts its properties, + // signs with it, and echoes the key back together with the signature. + const worker = new Worker(` + 'use strict'; + const { parentPort } = require('node:worker_threads'); + const { subtle } = globalThis.crypto; + parentPort.once('message', async ({ key, expected }) => { + try { + if (key.type !== expected.type || + key.extractable !== expected.extractable || + key.algorithm.name !== expected.algorithm.name || + key.algorithm.hash?.name !== expected.algorithm.hash?.name) { + throw new Error('slot mismatch in worker'); + } + const sig = await subtle.sign('HMAC', key, Buffer.from('wdata')); + // Echo the key back so the parent can verify round-trip. + parentPort.postMessage({ key, sig: Buffer.from(sig) }); + } catch (err) { + parentPort.postMessage({ error: err.message }); + } + }); + `, { eval: true }); + + worker.postMessage({ + key, + expected: { + type: key.type, + extractable: key.extractable, + algorithm: { + name: key.algorithm.name, + hash: key.algorithm.hash ? { name: key.algorithm.hash.name } : undefined, + }, + }, + }); + const [msg] = await once(worker, 'message'); + await worker.terminate(); + + assert.strictEqual(msg.error, undefined, msg.error); + // The key echoed back from the worker must itself be a fully-formed + // CryptoKey with all slots preserved. + assertSameCryptoKey(key, msg.key); + // The signature produced inside the worker must verify against the + // parent-side key. + assert.strictEqual( + await subtle.verify('HMAC', key, msg.sig, Buffer.from('wdata')), + true); +} + +async function checkRsaPssTransferToWorker({ publicKey, privateKey }) { + const data = Buffer.from('rsa-pss worker payload'); + const algorithm = { name: 'RSA-PSS', saltLength: 32 }; + const parentSignature = Buffer.from( + await subtle.sign(algorithm, privateKey, data)); + + const worker = new Worker(` + 'use strict'; + const assert = require('node:assert'); + const { parentPort } = require('node:worker_threads'); + const { subtle } = globalThis.crypto; + + ${describeKey} + + parentPort.once('message', async (message) => { + try { + const { + publicKey, + privateKey, + expectedPublic, + expectedPrivate, + signature, + data, + } = message; + assert.deepStrictEqual(describeKey(publicKey), expectedPublic); + assert.deepStrictEqual(describeKey(privateKey), expectedPrivate); + + const algorithm = { name: 'RSA-PSS', saltLength: 32 }; + const verified = await subtle.verify( + algorithm, publicKey, signature, data); + const workerSignature = Buffer.from( + await subtle.sign(algorithm, privateKey, data)); + + parentPort.postMessage({ + publicKey, + privateKey, + verified, + signature: workerSignature, + }); + } catch (err) { + parentPort.postMessage({ error: err.stack || err.message }); + } + }); + `, { eval: true }); + + worker.postMessage({ + publicKey, + privateKey, + expectedPublic: describeKey(publicKey), + expectedPrivate: describeKey(privateKey), + signature: parentSignature, + data, + }); + const [msg] = await once(worker, 'message'); + await worker.terminate(); + + assert.strictEqual(msg.error, undefined, msg.error); + assert.strictEqual(msg.verified, true); + assertSameCryptoKey(publicKey, msg.publicKey); + assertSameCryptoKey(privateKey, msg.privateKey); + assert.strictEqual( + await subtle.verify(algorithm, publicKey, msg.signature, data), + true); +} + +(async () => { + // Extractable HMAC (secret) + const hmacExtractable = await subtle.importKey( + 'raw', + Buffer.from( + '000102030405060708090a0b0c0d0e0f' + + '101112131415161718191a1b1c1d1e1f', 'hex'), + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify']); + await checkHmacKey(hmacExtractable); + await checkTransferToWorker(hmacExtractable); + + // Non-extractable HMAC (secret) + const hmacNonExtractable = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-384' }, + false, + ['sign', 'verify']); + await checkHmacKey(hmacNonExtractable); + await checkTransferToWorker(hmacNonExtractable); + + // AES-GCM secret key + { + const key = await subtle.generateKey( + { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']); + const cloned = structuredClone(key); + assertSameCryptoKey(key, cloned); + const viaPort = await roundTripViaMessageChannel(key); + assertSameCryptoKey(key, viaPort); + const clonedAgain = structuredClone(viaPort); + assertSameCryptoKey(key, clonedAgain); + + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const plaintext = Buffer.from('secret payload'); + const ciphertext = await subtle.encrypt( + { name: 'AES-GCM', iv }, key, plaintext); + // Decrypt with every clone. + for (const k of [cloned, viaPort, clonedAgain]) { + const decrypted = await subtle.decrypt( + { name: 'AES-GCM', iv }, k, ciphertext); + assert.deepStrictEqual(Buffer.from(decrypted), plaintext); + } + } + + // ECDSA keypair (public extractable, private non-extractable) + const ecKeypair = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign', 'verify']); + await checkAsymmetricKeyPair(ecKeypair); + + // ECDSA with extractable private key (covers the extractable-private path) + const ecKeypairExtractable = await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-384' }, + true, + ['sign', 'verify']); + await checkAsymmetricKeyPair(ecKeypairExtractable); + + // RSA-PSS keypair through a Worker (covers public/private native key + // handles and cloning of the publicExponent algorithm member). + const rsaPssKeypair = await subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + false, + ['sign', 'verify']); + await checkRsaPssTransferToWorker(rsaPssKeypair); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-hidden-slots.js b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js new file mode 100644 index 00000000000000..792a1a59c4c5eb --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-hidden-slots.js @@ -0,0 +1,226 @@ +'use strict'; + +// CryptoKey prototype getters (`type`, `extractable`, +// `algorithm`, `usages`) are configurable in the Web Crypto IDL and +// can therefore be replaced by user code. Internal consumers of those +// attributes, and the custom inspect output, must NOT go through the +// public prototype getters, they must read the underlying native +// internal slots directly. This test mutates the prototype getters +// (and mutates the per-instance `algorithm`/`usages` caches returned +// by those getters) and asserts that: +// +// 1. util.inspect() shows the real internal values, unaffected by +// the replacement getters. +// 2. Internal Web Crypto and Node.js crypto bridge operations that +// receive the mutated CryptoKey still succeed and observe the real +// internal state, not the replaced one. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { + createHmac, + KeyObject, + sign: cryptoSign, + verify: cryptoVerify, +} = require('node:crypto'); +const { inspect } = require('node:util'); +const { subtle } = globalThis.crypto; + +common.expectWarning({ + DeprecationWarning: { + DEP0203: 'Passing a CryptoKey to node:crypto functions is deprecated.', + }, +}); + +(async () => { + const key = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify'], + ); + const { publicKey: ecPublicKey, privateKey: ecPrivateKey } = + await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + false, + ['sign', 'verify'], + ); + const { publicKey: rsaPublicKey } = await subtle.generateKey( + { + name: 'RSA-PSS', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-256', + }, + true, + ['sign', 'verify'], + ); + + // Public algorithm/usages objects are mutable, but they must be + // separate from the native-backed internal slots. + rsaPublicKey.algorithm.name = 'FORGED-ALGORITHM'; + rsaPublicKey.algorithm.hash.name = 'FORGED-HASH'; + rsaPublicKey.algorithm.publicExponent[0] = 0xff; + rsaPublicKey.usages.push('forged-usage'); + + const clonedRsaPublicKey = structuredClone(rsaPublicKey); + assert.strictEqual(clonedRsaPublicKey.algorithm.name, 'RSA-PSS'); + assert.strictEqual(clonedRsaPublicKey.algorithm.hash.name, 'SHA-256'); + assert.deepStrictEqual( + clonedRsaPublicKey.algorithm.publicExponent, + new Uint8Array([1, 0, 1])); + assert.deepStrictEqual(clonedRsaPublicKey.usages, ['verify']); + + const rsaJwk = await subtle.exportKey('jwk', rsaPublicKey); + assert.strictEqual(rsaJwk.alg, 'PS256'); + assert.deepStrictEqual(rsaJwk.key_ops, ['verify']); + + Object.defineProperties(Object.prototype, { + hash: { + configurable: true, + value: { name: 'FORGED-HASH' }, + }, + publicExponent: { + configurable: true, + value: new Uint8Array([0xff]), + }, + }); + + try { + const aesKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, + true, + ['encrypt'], + ); + assert.deepStrictEqual(aesKey.algorithm, { + name: 'AES-GCM', + length: 128, + }); + assert.strictEqual(Object.hasOwn(aesKey.algorithm, 'hash'), false); + assert.strictEqual( + Object.hasOwn(aesKey.algorithm, 'publicExponent'), + false); + + const clonedAesKey = structuredClone(aesKey); + assert.deepStrictEqual(clonedAesKey.algorithm, { + name: 'AES-GCM', + length: 128, + }); + } finally { + delete Object.prototype.hash; + delete Object.prototype.publicExponent; + } + + // Snapshot the real values BEFORE tampering. + const realType = key.type; + const realExtractable = key.extractable; + const realAlgorithm = { ...key.algorithm, hash: { ...key.algorithm.hash } }; + const realUsages = [...key.usages]; + + // 1) Replace all four prototype getters. + let proto = Object.getPrototypeOf(key); + while (proto && !Object.getOwnPropertyDescriptor(proto, 'type')) { + proto = Object.getPrototypeOf(proto); + } + assert.ok(proto, 'could not find CryptoKey.prototype'); + const forgedDescriptors = { + type: { + configurable: true, + enumerable: true, + get() { return 'FORGED-TYPE'; }, + }, + extractable: { + configurable: true, + enumerable: true, + get() { return 'FORGED-EXTRACTABLE'; }, + }, + algorithm: { + configurable: true, + enumerable: true, + get() { return { name: 'FORGED-ALGORITHM', hash: { name: 'FORGED-HASH' } }; }, + }, + usages: { + configurable: true, + enumerable: true, + get() { return ['forged-usage']; }, + }, + }; + const originalDescriptors = {}; + for (const [name, descriptor] of Object.entries(forgedDescriptors)) { + originalDescriptors[name] = Object.getOwnPropertyDescriptor(proto, name); + Object.defineProperty(proto, name, descriptor); + } + + try { + // Confirm the forgeries are in effect from user-code perspective. + assert.strictEqual(key.type, 'FORGED-TYPE'); + assert.strictEqual(key.extractable, 'FORGED-EXTRACTABLE'); + assert.strictEqual(key.algorithm.name, 'FORGED-ALGORITHM'); + assert.deepStrictEqual(key.usages, ['forged-usage']); + + // 2) util.inspect() must not be influenced by the forged getters, + // it must read the real internal slots directly. + const rendered = inspect(key, { depth: 4 }); + assert.match(rendered, /type: 'secret'/); + assert.match(rendered, /extractable: true/); + assert.match(rendered, /name: 'HMAC'/); + assert.match(rendered, /name: 'SHA-256'/); + assert.match(rendered, /'sign'/); + assert.match(rendered, /'verify'/); + assert.doesNotMatch(rendered, /FORGED/); + + // 3) Internal consumers that receive this CryptoKey must see the + // real internal slots. exportKey('jwk') reads [[type]], + // [[extractable]], [[algorithm]], and [[usages]]; if any + // went through the user-visible getter the call would either + // throw or produce forged output. + const jwk = await subtle.exportKey('jwk', key); + assert.strictEqual(jwk.kty, 'oct'); + assert.strictEqual(jwk.alg, 'HS256'); + assert.strictEqual(jwk.ext, true); + assert.deepStrictEqual(jwk.key_ops.sort(), ['sign', 'verify']); + + // 4) The Node.js crypto bridge must also read the real native + // slots directly, both for KeyObject.from() and for deprecated + // direct CryptoKey consumption. + const keyObject = KeyObject.from(key); + assert.strictEqual(keyObject.type, 'secret'); + assert.deepStrictEqual(keyObject.export(), Buffer.from(jwk.k, 'base64url')); + + const payload = Buffer.from('payload'); + const digest = createHmac('sha256', key).update(payload).digest('hex'); + const expectedDigest = + createHmac('sha256', keyObject).update(payload).digest('hex'); + assert.strictEqual(digest, expectedDigest); + + const signature = cryptoSign('sha256', payload, ecPrivateKey); + assert.strictEqual( + cryptoVerify('sha256', payload, ecPublicKey, signature), + true); + + // 5) Importing back from the exported JWK must yield an equivalent + // key, i.e. the real algorithm and usages round-trip. + const reimported = await subtle.importKey('jwk', jwk, + { name: 'HMAC', hash: 'SHA-256' }, + true, ['sign', 'verify']); + // Reimported's prototype is the same mutated prototype, so we must + // also inspect() the reimported key to check real slots. + const rerendered = inspect(reimported, { depth: 4 }); + assert.match(rerendered, /type: 'secret'/); + assert.match(rerendered, /name: 'HMAC'/); + assert.doesNotMatch(rerendered, /FORGED/); + } finally { + // Restore the original getters so subsequent tests are unaffected. + for (const [name, descriptor] of Object.entries(originalDescriptors)) { + Object.defineProperty(proto, name, descriptor); + } + } + + // After restoration, the real values come back through the public API. + assert.strictEqual(key.type, realType); + assert.strictEqual(key.extractable, realExtractable); + assert.deepStrictEqual(key.algorithm, realAlgorithm); + assert.deepStrictEqual(key.usages, realUsages); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js b/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js new file mode 100644 index 00000000000000..44e3413d17ca3b --- /dev/null +++ b/test/parallel/test-webcrypto-cryptokey-no-own-symbols.js @@ -0,0 +1,52 @@ +'use strict'; + +// CryptoKey instances must not expose any own Symbol-keyed properties +// to user code. The internal slots backing the public getters are +// kept in native internal fields, with JS-side caches in private fields, +// so that reflection APIs such as Object.getOwnPropertySymbols() and +// Reflect.ownKeys() cannot enumerate them, even after the public getters +// have been invoked and their per-instance caches populated. + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('node:assert'); +const { subtle } = globalThis.crypto; + +(async () => { + const keys = [ + await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']), + (await subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])).privateKey, + (await subtle.generateKey( + { name: 'RSA-PSS', modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, + true, ['sign', 'verify'])).publicKey, + await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, true, ['encrypt', 'decrypt']), + ]; + + for (const key of keys) { + // Touch every public getter so any lazy per-instance caching would + // materialise now. + /* eslint-disable no-unused-expressions */ + key.type; + key.extractable; + key.algorithm; + key.usages; + // Read the getters a second time to exercise the cache-hit path. + key.algorithm; + key.usages; + /* eslint-enable no-unused-expressions */ + + assert.deepStrictEqual( + Object.getOwnPropertySymbols(key), [], + `CryptoKey has own Symbol properties: ${ + Object.getOwnPropertySymbols(key).map(String).join(', ')}`, + ); + assert.deepStrictEqual(Object.getOwnPropertyNames(key), []); + assert.deepStrictEqual(Reflect.ownKeys(key), []); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-deduplicate-usages.js b/test/parallel/test-webcrypto-deduplicate-usages.js index cf24cb896e0535..ebd1b4683fc6f7 100644 --- a/test/parallel/test-webcrypto-deduplicate-usages.js +++ b/test/parallel/test-webcrypto-deduplicate-usages.js @@ -42,17 +42,13 @@ function assertSameSet(actual, expected, msg) { { 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 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + { algorithm: { name: 'ChaCha20-Poly1305' }, + usages: ['wrapKey', 'decrypt', 'encrypt', 'unwrapKey', 'wrapKey', 'encrypt'], + expected: ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { symmetric.push({ @@ -69,16 +65,6 @@ function assertSameSet(actual, expected, msg) { 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); @@ -121,7 +107,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { asymmetric.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -136,7 +122,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['decapsulateKey', 'decapsulateBits'], }); } else { - common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of asymmetric) { @@ -172,17 +158,10 @@ function assertSameSet(actual, expected, msg) { { 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), + { 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'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { // KMAC does not support `raw` format, only `raw-secret` and `jwk`. @@ -310,7 +289,7 @@ function assertSameSet(actual, expected, msg) { assert.deepStrictEqual(imported.usages, ['sign']); })()); - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { // ML-DSA JWK roundtrip. tests.push((async () => { const { privateKey } = await subtle.generateKey( @@ -322,7 +301,7 @@ function assertSameSet(actual, expected, msg) { assert.deepStrictEqual(imported.usages, ['sign']); })()); } else { - common.printSkipMessage('ML-DSA requires OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } // Spki import of RSA public key. @@ -342,20 +321,16 @@ function assertSameSet(actual, expected, msg) { })()); // 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'); - } + 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); + })()); // AES-OCB raw-secret import. if (hasOpenSSL(3)) { @@ -441,17 +416,10 @@ function assertSameSet(actual, expected, msg) { { 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 }, + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey', 'wrapKey', 'unwrapKey'], - expected: ['wrapKey', 'unwrapKey'], - }); - } else { - common.printSkipMessage('AES-KW is not supported in BoringSSL'); - } + expected: ['wrapKey', 'unwrapKey'] }, + ]; if (hasOpenSSL(3)) { jwkVectors.push({ @@ -509,7 +477,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['deriveKey', 'deriveBits'] }, ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { jwkPairVectors.push({ algorithm: { name: 'ML-DSA-65' }, usages: ['verify', 'sign', 'verify', 'sign'], @@ -517,7 +485,7 @@ function assertSameSet(actual, expected, msg) { privateExpected: ['sign'], }); } else { - common.printSkipMessage('ML-DSA requires OpenSSL >= 3.5'); + common.printSkipMessage('ML-DSA and ML-KEM require OpenSSL >= 3.5 or BoringSSL'); } for (const { algorithm, usages, publicExpected, privateExpected } of jwkPairVectors) { diff --git a/test/parallel/test-webcrypto-derivebits-argon2.js b/test/parallel/test-webcrypto-derivebits-argon2.js index b03447bb1e70b9..e2b465ab206bec 100644 --- a/test/parallel/test-webcrypto-derivebits-argon2.js +++ b/test/parallel/test-webcrypto-derivebits-argon2.js @@ -90,3 +90,36 @@ for (const { algorithm, length, password, params, tag } of vectors) { } })().then(common.mustCall()); } + +{ + (async () => { + const algorithm = { + name: 'Argon2id', + memory: 32, + passes: 3, + parallelism: 4, + nonce: Buffer.alloc(16, 0x02), + }; + const key = await subtle.importKey( + 'raw-secret', + Buffer.alloc(32, 0x01), + algorithm.name, + false, + ['deriveBits']); + + const omitted = await subtle.deriveBits(algorithm, key, 256); + const explicitEmpty = await subtle.deriveBits({ + ...algorithm, + secretValue: Buffer.alloc(0), + associatedData: Buffer.alloc(0), + }, key, 256); + assert.deepStrictEqual(omitted, explicitEmpty); + + await assert.rejects( + subtle.deriveBits({ ...algorithm, passes: 0 }, key, 256), + { + name: 'OperationError', + message: 'passes must be > 0', + }); + })().then(common.mustCall()); +} diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 2759223e76a060..d2057d1f782e7f 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -24,12 +24,12 @@ const kDerivedKeyTypes = [ ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; if (!process.features.openssl_is_boringssl) { kDerivedKeyTypes.push( - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ['HMAC', 256, 'SHA3-256', 'sign', 'verify'], ['HMAC', 256, 'SHA3-384', 'sign', 'verify'], ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], @@ -628,16 +628,27 @@ async function testWrongKeyType( })().then(common.mustCall()); // https://github.com/w3c/webcrypto/pull/380 -{ - crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']).then((key) => { - return crypto.subtle.deriveBits({ - name: 'HKDF', - hash: { name: 'SHA-256' }, - info: new Uint8Array(0), - salt: new Uint8Array(0), - }, key, 0); - }).then((bits) => { - assert.deepStrictEqual(bits, new ArrayBuffer(0)); - }) - .then(common.mustCall()); -} +(async function() { + const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + const bits = await crypto.subtle.deriveBits({ + name: 'HKDF', + hash: { name: 'SHA-256' }, + info: new Uint8Array(0), + salt: new Uint8Array(0), + }, key, 0); + assert.deepStrictEqual(bits, new ArrayBuffer(0)); +})().then(common.mustCall()); + +// OpenSSL limits info to 1024 bytes +(async function() { + const key = await crypto.subtle.importKey('raw', new Uint8Array(0), 'HKDF', false, ['deriveBits']); + await assert.rejects(crypto.subtle.deriveBits({ + name: 'HKDF', + hash: { name: 'SHA-256' }, + info: new Uint8Array(1025), + salt: new Uint8Array(0), + }, key, 0), { + name: 'OperationError', + message: 'algorithm.info must be at most 1024 bytes', + }); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index e04a7eab1bd8ef..32c6a93efbbc4e 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -265,6 +265,36 @@ const { KeyObject } = require('crypto'); })().then(common.mustCall()); } +if (hasOpenSSL(3)) { + (async () => { + const derivedKeyAlgorithm = { name: 'KMAC128', length: 0 }; + const usages = ['sign']; + for (const [algorithm, baseKeyAlgorithm] of [ + [ + { name: 'HKDF', salt: new Uint8Array(), info: new Uint8Array(), hash: 'SHA-256' }, + { name: 'HKDF' }, + ], + [ + { name: 'PBKDF2', salt: new Uint8Array(), hash: 'SHA-256', iterations: 20 }, + { name: 'PBKDF2' }, + ], + ]) { + const baseKey = await subtle.importKey( + 'raw', + new Uint8Array(), + baseKeyAlgorithm, + false, + ['deriveKey']); + await assert.rejects( + subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), + { + name: 'DataError', + message: /KmacImportParams\.length cannot be 0/, + }); + } + })().then(common.mustCall()); +} + // Test X25519 and X448 key derivation { async function test(name) { diff --git a/test/parallel/test-webcrypto-digest-turboshake-rfc.js b/test/parallel/test-webcrypto-digest-turboshake-rfc.js new file mode 100644 index 00000000000000..43762fecc2c41e --- /dev/null +++ b/test/parallel/test-webcrypto-digest-turboshake-rfc.js @@ -0,0 +1,399 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +// RFC 9861 Section 5 test vectors + +// Generates a Buffer of length n by repeating the pattern 00 01 02 .. F9 FA. +function ptn(n) { + const buf = Buffer.allocUnsafe(n); + for (let i = 0; i < n; i++) + buf[i] = i % 251; + return buf; +} + +assert.deepStrictEqual( + ptn(17 ** 2).toString('hex'), + '000102030405060708090a0b0c0d0e0f' + + '101112131415161718191a1b1c1d1e1f' + + '202122232425262728292a2b2c2d2e2f' + + '303132333435363738393a3b3c3d3e3f' + + '404142434445464748494a4b4c4d4e4f' + + '505152535455565758595a5b5c5d5e5f' + + '606162636465666768696a6b6c6d6e6f' + + '707172737475767778797a7b7c7d7e7f' + + '808182838485868788898a8b8c8d8e8f' + + '909192939495969798999a9b9c9d9e9f' + + 'a0a1a2a3a4a5a6a7a8a9aaabacadaeaf' + + 'b0b1b2b3b4b5b6b7b8b9babbbcbdbebf' + + 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf' + + 'd0d1d2d3d4d5d6d7d8d9dadbdcdddedf' + + 'e0e1e2e3e4e5e6e7e8e9eaebecedeeef' + + 'f0f1f2f3f4f5f6f7f8f9fa0001020304' + + '05060708090a0b0c0d0e0f1011121314' + + '15161718191a1b1c1d1e1f2021222324' + + '25', +); + +const turboSHAKE128Vectors = [ + // [input, outputLengthBytes, expected(, domainSeparation)] + [new Uint8Array(0), 32, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c'], + [new Uint8Array(0), 64, + '1e415f1c5983aff2169217277d17bb53' + + '8cd945a397ddec541f1ce41af2c1b74c' + + '3e8ccae2a4dae56c84a04c2385c03c15' + + 'e8193bdf58737363321691c05462c8df'], + [ptn(17 ** 0), 32, + '55cedd6f60af7bb29a4042ae832ef3f5' + + '8db7299f893ebb9247247d856958daa9'], + [ptn(17 ** 1), 32, + '9c97d036a3bac819db70ede0ca554ec6' + + 'e4c2a1a4ffbfd9ec269ca6a111161233'], + [ptn(17 ** 2), 32, + '96c77c279e0126f7fc07c9b07f5cdae1' + + 'e0be60bdbe10620040e75d7223a624d2'], + [ptn(17 ** 3), 32, + 'd4976eb56bcf118520582b709f73e1d6' + + '853e001fdaf80e1b13e0d0599d5fb372'], + [ptn(17 ** 4), 32, + 'da67c7039e98bf530cf7a37830c6664e' + + '14cbab7f540f58403b1b82951318ee5c'], + [ptn(17 ** 5), 32, + 'b97a906fbf83ef7c812517abf3b2d0ae' + + 'a0c4f60318ce11cf103925127f59eecd'], + [ptn(17 ** 6), 32, + '35cd494adeded2f25239af09a7b8ef0c' + + '4d1ca4fe2d1ac370fa63216fe7b4c2b1'], + [Buffer.from('ffffff', 'hex'), 32, + 'bf323f940494e88ee1c540fe660be8a0' + + 'c93f43d15ec006998462fa994eed5dab', 0x01], + [Buffer.from('ff', 'hex'), 32, + '8ec9c66465ed0d4a6c35d13506718d68' + + '7a25cb05c74cca1e42501abd83874a67', 0x06], + [Buffer.from('ffffff', 'hex'), 32, + 'b658576001cad9b1e5f399a9f77723bb' + + 'a05458042d68206f7252682dba3663ed', 0x07], + [Buffer.from('ffffffffffffff', 'hex'), 32, + '8deeaa1aec47ccee569f659c21dfa8e1' + + '12db3cee37b18178b2acd805b799cc37', 0x0b], + [Buffer.from('ff', 'hex'), 32, + '553122e2135e363c3292bed2c6421fa2' + + '32bab03daa07c7d6636603286506325b', 0x30], + [Buffer.from('ffffff', 'hex'), 32, + '16274cc656d44cefd422395d0f9053bd' + + 'a6d28e122aba15c765e5ad0e6eaf26f9', 0x7f], +]; + +const turboSHAKE256Vectors = [ + // [input, outputLengthBytes, expected(, domainSeparation)] + [new Uint8Array(0), 64, + '367a329dafea871c7802ec67f905ae13' + + 'c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f00' + + '2fbafabb6e13ec1cc20d995547600db0'], + [ptn(17 ** 0), 64, + '3e1712f928f8eaf1054632b2aa0a246e' + + 'd8b0c378728f60bc970410155c28820e' + + '90cc90d8a3006aa2372c5c5ea176b068' + + '2bf22bae7467ac94f74d43d39b0482e2'], + [ptn(17 ** 1), 64, + 'b3bab0300e6a191fbe61379398359235' + + '78794ea54843f5011090fa2f3780a9e5' + + 'cb22c59d78b40a0fbff9e672c0fbe097' + + '0bd2c845091c6044d687054da5d8e9c7'], + [ptn(17 ** 2), 64, + '66b810db8e90780424c0847372fdc957' + + '10882fde31c6df75beb9d4cd9305cfca' + + 'e35e7b83e8b7e6eb4b78605880116316' + + 'fe2c078a09b94ad7b8213c0a738b65c0'], + [ptn(17 ** 3), 64, + 'c74ebc919a5b3b0dd1228185ba02d29e' + + 'f442d69d3d4276a93efe0bf9a16a7dc0' + + 'cd4eabadab8cd7a5edd96695f5d360ab' + + 'e09e2c6511a3ec397da3b76b9e1674fb'], + [ptn(17 ** 4), 64, + '02cc3a8897e6f4f6ccb6fd46631b1f52' + + '07b66c6de9c7b55b2d1a23134a170afd' + + 'ac234eaba9a77cff88c1f020b7372461' + + '8c5687b362c430b248cd38647f848a1d'], + [ptn(17 ** 5), 64, + 'add53b06543e584b5823f626996aee50' + + 'fe45ed15f20243a7165485acb4aa76b4' + + 'ffda75cedf6d8cdc95c332bd56f4b986' + + 'b58bb17d1778bfc1b1a97545cdf4ec9f'], + [ptn(17 ** 6), 64, + '9e11bc59c24e73993c1484ec66358ef7' + + '1db74aefd84e123f7800ba9c4853e02c' + + 'fe701d9e6bb765a304f0dc34a4ee3ba8' + + '2c410f0da70e86bfbd90ea877c2d6104'], + [Buffer.from('ffffff', 'hex'), 64, + 'd21c6fbbf587fa2282f29aea620175fb' + + '0257413af78a0b1b2a87419ce031d933' + + 'ae7a4d383327a8a17641a34f8a1d1003' + + 'ad7da6b72dba84bb62fef28f62f12424', 0x01], + [Buffer.from('ff', 'hex'), 64, + '738d7b4e37d18b7f22ad1b5313e357e3' + + 'dd7d07056a26a303c433fa3533455280' + + 'f4f5a7d4f700efb437fe6d281405e07b' + + 'e32a0a972e22e63adc1b090daefe004b', 0x06], + [Buffer.from('ffffff', 'hex'), 64, + '18b3b5b7061c2e67c1753a00e6ad7ed7' + + 'ba1c906cf93efb7092eaf27fbeebb755' + + 'ae6e292493c110e48d260028492b8e09' + + 'b5500612b8f2578985ded5357d00ec67', 0x07], + [Buffer.from('ffffffffffffff', 'hex'), 64, + 'bb36764951ec97e9d85f7ee9a67a7718' + + 'fc005cf42556be79ce12c0bde50e5736' + + 'd6632b0d0dfb202d1bbb8ffe3dd74cb0' + + '0834fa756cb03471bab13a1e2c16b3c0', 0x0b], + [Buffer.from('ff', 'hex'), 64, + 'f3fe12873d34bcbb2e608779d6b70e7f' + + '86bec7e90bf113cbd4fdd0c4e2f4625e' + + '148dd7ee1a52776cf77f240514d9ccfc' + + '3b5ddab8ee255e39ee389072962c111a', 0x30], + [Buffer.from('ffffff', 'hex'), 64, + 'abe569c1f77ec340f02705e7d37c9ab7' + + 'e155516e4a6a150021d70b6fac0bb40c' + + '069f9a9828a0d575cd99f9bae435ab1a' + + 'cf7ed9110ba97ce0388d074bac768776', 0x7f], +]; + +const kt128Vectors = [ + // [input, outputLengthBytes, expected(, customization)] + [new Uint8Array(0), 32, + '1ac2d450fc3b4205d19da7bfca1b3751' + + '3c0803577ac7167f06fe2ce1f0ef39e5'], + [new Uint8Array(0), 64, + '1ac2d450fc3b4205d19da7bfca1b3751' + + '3c0803577ac7167f06fe2ce1f0ef39e5' + + '4269c056b8c82e48276038b6d292966c' + + 'c07a3d4645272e31ff38508139eb0a71'], + [ptn(1), 32, + '2bda92450e8b147f8a7cb629e784a058' + + 'efca7cf7d8218e02d345dfaa65244a1f'], + [ptn(17), 32, + '6bf75fa2239198db4772e36478f8e19b' + + '0f371205f6a9a93a273f51df37122888'], + [ptn(17 ** 2), 32, + '0c315ebcdedbf61426de7dcf8fb725d1' + + 'e74675d7f5327a5067f367b108ecb67c'], + [ptn(17 ** 3), 32, + 'cb552e2ec77d9910701d578b457ddf77' + + '2c12e322e4ee7fe417f92c758f0d59d0'], + [ptn(17 ** 4), 32, + '8701045e22205345ff4dda05555cbb5c' + + '3af1a771c2b89baef37db43d9998b9fe'], + [ptn(17 ** 5), 32, + '844d610933b1b9963cbdeb5ae3b6b05c' + + 'c7cbd67ceedf883eb678a0a8e0371682'], + [ptn(17 ** 6), 32, + '3c390782a8a4e89fa6367f72feaaf132' + + '55c8d95878481d3cd8ce85f58e880af8'], + [new Uint8Array(0), 32, + 'fab658db63e94a246188bf7af69a1330' + + '45f46ee984c56e3c3328caaf1aa1a583', ptn(1)], + [Buffer.from('ff', 'hex'), 32, + 'd848c5068ced736f4462159b9867fd4c' + + '20b808acc3d5bc48e0b06ba0a3762ec4', ptn(41)], + [Buffer.from('ffffff', 'hex'), 32, + 'c389e5009ae57120854c2e8c64670ac0' + + '1358cf4c1baf89447a724234dc7ced74', ptn(41 ** 2)], + [Buffer.from('ffffffffffffff', 'hex'), 32, + '75d2f86a2e644566726b4fbcfc5657b9' + + 'dbcf070c7b0dca06450ab291d7443bcf', ptn(41 ** 3)], + [ptn(8191), 32, + '1b577636f723643e990cc7d6a6598374' + + '36fd6a103626600eb8301cd1dbe553d6'], + [ptn(8192), 32, + '48f256f6772f9edfb6a8b661ec92dc93' + + 'b95ebd05a08a17b39ae3490870c926c3'], + [ptn(8192), 32, + '3ed12f70fb05ddb58689510ab3e4d23c' + + '6c6033849aa01e1d8c220a297fedcd0b', ptn(8189)], + [ptn(8192), 32, + '6a7c1b6a5cd0d8c9ca943a4a216cc646' + + '04559a2ea45f78570a15253d67ba00ae', ptn(8190)], +]; + +const kt256Vectors = [ + // [input, outputLengthBytes, expected(, customization)] + [new Uint8Array(0), 64, + 'b23d2e9cea9f4904e02bec06817fc10c' + + 'e38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa3348235' + + '3fd4adc7148ecb782855003aaebde4a9'], + [new Uint8Array(0), 128, + 'b23d2e9cea9f4904e02bec06817fc10c' + + 'e38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa3348235' + + '3fd4adc7148ecb782855003aaebde4a9' + + 'b0925319d8ea1e121a609821ec19efea' + + '89e6d08daee1662b69c840289f188ba8' + + '60f55760b61f82114c030c97e5178449' + + '608ccd2cd2d919fc7829ff69931ac4d0'], + [ptn(1), 64, + '0d005a194085360217128cf17f91e1f7' + + '1314efa5564539d444912e3437efa17f' + + '82db6f6ffe76e781eaa068bce01f2bbf' + + '81eacb983d7230f2fb02834a21b1ddd0'], + [ptn(17), 64, + '1ba3c02b1fc514474f06c8979978a905' + + '6c8483f4a1b63d0dccefe3a28a2f323e' + + '1cdcca40ebf006ac76ef039715234683' + + '7b1277d3e7faa9c9653b19075098527b'], + [ptn(17 ** 2), 64, + 'de8ccbc63e0f133ebb4416814d4c66f6' + + '91bbf8b6a61ec0a7700f836b086cb029' + + 'd54f12ac7159472c72db118c35b4e6aa' + + '213c6562caaa9dcc518959e69b10f3ba'], + [ptn(17 ** 3), 64, + '647efb49fe9d717500171b41e7f11bd4' + + '91544443209997ce1c2530d15eb1ffbb' + + '598935ef954528ffc152b1e4d731ee26' + + '83680674365cd191d562bae753b84aa5'], + [ptn(17 ** 4), 64, + 'b06275d284cd1cf205bcbe57dccd3ec1' + + 'ff6686e3ed15776383e1f2fa3c6ac8f0' + + '8bf8a162829db1a44b2a43ff83dd89c3' + + 'cf1ceb61ede659766d5ccf817a62ba8d'], + [ptn(17 ** 5), 64, + '9473831d76a4c7bf77ace45b59f1458b' + + '1673d64bcd877a7c66b2664aa6dd149e' + + '60eab71b5c2bab858c074ded81ddce2b' + + '4022b5215935c0d4d19bf511aeeb0772'], + [ptn(17 ** 6), 64, + '0652b740d78c5e1f7c8dcc1777097382' + + '768b7ff38f9a7a20f29f413bb1b3045b' + + '31a5578f568f911e09cf44746da84224' + + 'a5266e96a4a535e871324e4f9c7004da'], + [new Uint8Array(0), 64, + '9280f5cc39b54a5a594ec63de0bb9937' + + '1e4609d44bf845c2f5b8c316d72b1598' + + '11f748f23e3fabbe5c3226ec96c62186' + + 'df2d33e9df74c5069ceecbb4dd10eff6', ptn(1)], + [Buffer.from('ff', 'hex'), 64, + '47ef96dd616f200937aa7847e34ec2fe' + + 'ae8087e3761dc0f8c1a154f51dc9ccf8' + + '45d7adbce57ff64b639722c6a1672e3b' + + 'f5372d87e00aff89be97240756998853', ptn(41)], + [Buffer.from('ffffff', 'hex'), 64, + '3b48667a5051c5966c53c5d42b95de45' + + '1e05584e7806e2fb765eda959074172c' + + 'b438a9e91dde337c98e9c41bed94c4e0' + + 'aef431d0b64ef2324f7932caa6f54969', ptn(41 ** 2)], + [Buffer.from('ffffffffffffff', 'hex'), 64, + 'e0911cc00025e1540831e266d94add9b' + + '98712142b80d2629e643aac4efaf5a3a' + + '30a88cbf4ac2a91a2432743054fbcc98' + + '97670e86ba8cec2fc2ace9c966369724', ptn(41 ** 3)], + [ptn(8191), 64, + '3081434d93a4108d8d8a3305b89682ce' + + 'bedc7ca4ea8a3ce869fbb73cbe4a58ee' + + 'f6f24de38ffc170514c70e7ab2d01f03' + + '812616e863d769afb3753193ba045b20'], + [ptn(8192), 64, + 'c6ee8e2ad3200c018ac87aaa031cdac2' + + '2121b412d07dc6e0dccbb53423747e9a' + + '1c18834d99df596cf0cf4b8dfafb7bf0' + + '2d139d0c9035725adc1a01b7230a41fa'], + [ptn(8192), 64, + '74e47879f10a9c5d11bd2da7e194fe57' + + 'e86378bf3c3f7448eff3c576a0f18c5c' + + 'aae0999979512090a7f348af4260d4de' + + '3c37f1ecaf8d2c2c96c1d16c64b12496', ptn(8189)], + [ptn(8192), 64, + 'f4b5908b929ffe01e0f79ec2f21243d4' + + '1a396b2e7303a6af1d6399cd6c7a0a2d' + + 'd7c4f607e8277f9c9b1cb4ab9ddc59d4' + + 'b92d1fc7558441f1832c3279a4241b8b', ptn(8190)], +]; + +async function checkDigest(name, vectors) { + const isKT = name.startsWith('KT'); + for (const [input, outputLength, expected, ...rest] of vectors) { + const algorithm = { name, outputLength: outputLength * 8 }; + if (rest.length) { + if (isKT) + algorithm.customization = rest[0]; + else + algorithm.domainSeparation = rest[0]; + } + const result = await subtle.digest(algorithm, input); + assert.deepStrictEqual( + Buffer.from(result).toString('hex'), + expected, + ); + } +} + +(async () => { + await checkDigest('TurboSHAKE128', turboSHAKE128Vectors); + + // TurboSHAKE128(M=00^0, D=1F, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'TurboSHAKE128', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'a3b9b0385900ce761f22aed548e754da' + + '10a5242d62e8c658e3f3a923a7555607', + ); + } + + await checkDigest('TurboSHAKE256', turboSHAKE256Vectors); + + // TurboSHAKE256(M=00^0, D=1F, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'TurboSHAKE256', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'abefa11630c661269249742685ec082f' + + '207265dccf2f43534e9c61ba0c9d1d75', + ); + } + + await checkDigest('KT128', kt128Vectors); + + // KT128(M=00^0, C=00^0, 10032), last 32 bytes + { + const result = await subtle.digest({ + name: 'KT128', + outputLength: 10032 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-32).toString('hex'), + 'e8dc563642f7228c84684c898405d3a8' + + '34799158c079b12880277a1d28e2ff6d', + ); + } + + await checkDigest('KT256', kt256Vectors); + + // KT256(M=00^0, C=00^0, 10064), last 64 bytes + { + const result = await subtle.digest({ + name: 'KT256', + outputLength: 10064 * 8, + }, new Uint8Array(0)); + assert.deepStrictEqual( + Buffer.from(result).subarray(-64).toString('hex'), + 'ad4a1d718cf950506709a4c33396139b' + + '4449041fc79a05d68da35f1e453522e0' + + '56c64fe94958e7085f2964888259b993' + + '2752f3ccd855288efee5fcbb8b563069', + ); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-digest-turboshake.js b/test/parallel/test-webcrypto-digest-turboshake.js new file mode 100644 index 00000000000000..0b5586b19286be --- /dev/null +++ b/test/parallel/test-webcrypto-digest-turboshake.js @@ -0,0 +1,181 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const kSourceData = { + empty: '', + short: '156eea7cc14c56cb94db030a4a9d95ff', + medium: 'b6c8f9df648cd088b70f38e74197b18cb81e1e435' + + '0d50bccb8fb5a7379c87bb2e3d6ed5461ed1e9f36' + + 'f340a3962a446b815b794b4bd43a4403502077b22' + + '56cc807837f3aacd118eb4b9c2baeb897068625ab' + + 'aca193', + long: null +}; + +kSourceData.long = kSourceData.medium.repeat(1024); + +// Test vectors generated with PyCryptodome as a reference implementation +const kDigestedData = { + 'turboshake128': { + empty: '1e415f1c5983aff2169217277d17bb538cd945a397ddec541f1ce41af2c1b74c', + short: 'f8d1ebf3b48b71b0514686090eb25f1de322a00149be9b4dc5f09ac9077cd8a8', + medium: '0d0e7eceb4ae58c3c48f6c2bad56d0f8ff3f887468d3ea55a138aedf395233c0', + long: '5747c06f02ffd9d6c911b6453cc8b717083ab6417319a6ec5c3bb39ed0baf331', + }, + 'turboshake256': { + empty: '367a329dafea871c7802ec67f905ae13c57695dc2c6663c61035f59a18f8e7db' + + '11edc0e12e91ea60eb6b32df06dd7f002fbafabb6e13ec1cc20d995547600db0', + short: 'b47aa0a5b76caf9b10cfaeff036df0cdb86362d2bd036a2fee0cd0d74e79279c' + + 'b9c57a70da1e3dd9e126a469857ba4c82b0efb3ae06d1a3781a6f102c3eb3a1d', + medium: '7fa19fd828762d2dba6eea8407d1fb04302b5a4f1ca3d00b3672c1e3b3331d18' + + '925b7ec380f3f04673a164dab04d2a0c5c12818046284c38d286645741a8aa3e', + long: '12d0b90c08f588710733cc07f0a2d6ab0795a4a24904c111062226fcd9d5dcb2' + + '1d6b5b848c9aebbcab221f031e9b4ea71e099ec785e822b1b83e73d0750ca1a7', + }, + 'kt128': { + empty: '1ac2d450fc3b4205d19da7bfca1b37513c0803577ac7167f06fe2ce1f0ef39e5', + short: '4719a2ac1dc1c592521cf201df3f476ea496fe461abe9a2604527f6bec047579', + medium: '00f3add71679681720b925416953897ac62cfae97060dd5f2e1641a076580cc9', + long: 'c05805c2736deb4be3fca6e3717b9af0aa18ceeaaeeab66b328a3ffebf0a814d', + }, + 'kt256': { + empty: 'b23d2e9cea9f4904e02bec06817fc10ce38ce8e93ef4c89e6537076af8646404' + + 'e3e8b68107b8833a5d30490aa33482353fd4adc7148ecb782855003aaebde4a9', + short: '6709e5e312f2dee4547ecb0ab7d42728ba57985983731afbd6c2a0676c522274' + + 'cf9153064ee07982129d3f58d4dbe00050eb28b392559bdb020aca302b7a28cb', + medium: '9078b6ff78e9c4b3c8ff49e5b9f337b36cc6d6749d23985035886d993db69f7e' + + '05fea97125e0889130da09fc5837761f7793e3e44d85be1ee1f6af7f4a1f50cb', + long: '41f83b7c7d02fc6d98f1fa1474d765caff4673f90cd7204894d7da72d97403b6' + + '2fe5c4bae2bf0ce3dcd51e80c98bd25ce5fe54040259d9466b67f1517dac0712', + }, +}; + +function buildAlg(name) { + const lower = name.toLowerCase(); + if (lower.startsWith('turboshake')) { + const outputLength = lower === 'turboshake128' ? 256 : 512; + return { name, outputLength }; + } + if (lower.startsWith('kt')) { + const outputLength = lower === 'kt128' ? 256 : 512; + return { name, outputLength }; + } + return name; +} + +async function testDigest(size, alg) { + const digest = await subtle.digest( + alg, + Buffer.from(kSourceData[size], 'hex')); + + assert.strictEqual( + Buffer.from(digest).toString('hex'), + kDigestedData[(alg.name || alg).toLowerCase()][size]); +} + +// Known-answer tests +(async function() { + const variations = []; + Object.keys(kSourceData).forEach((size) => { + Object.keys(kDigestedData).forEach((alg) => { + const upCase = alg.toUpperCase(); + const downCase = alg.toLowerCase(); + const mixedCase = upCase.slice(0, 1) + downCase.slice(1); + + variations.push(testDigest(size, buildAlg(upCase))); + variations.push(testDigest(size, buildAlg(downCase))); + variations.push(testDigest(size, buildAlg(mixedCase))); + }); + }); + + await Promise.all(variations); +})().then(common.mustCall()); + +// Edge cases: zero-length output rejects +(async () => { + await assert.rejects( + subtle.digest({ name: 'TurboSHAKE128', outputLength: 0 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid TurboShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest({ name: 'KT128', outputLength: 0 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid KangarooTwelveParams outputLength', + }); +})().then(common.mustCall()); + +// Edge case: non-byte-aligned outputLength rejects +(async () => { + await assert.rejects( + subtle.digest({ name: 'TurboSHAKE128', outputLength: 7 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid TurboShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest({ name: 'KT128', outputLength: 7 }, Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid KangarooTwelveParams outputLength', + }); +})().then(common.mustCall()); + +// TurboSHAKE domain separation byte +(async () => { + // Domain separation 0x07 should produce different output than default 0x1F + const [d07, d1f] = await Promise.all([ + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x07 }, + Buffer.alloc(0)), + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256 }, + Buffer.alloc(0)), + ]); + assert.notDeepStrictEqual( + new Uint8Array(d07), + new Uint8Array(d1f)); + + // Verify D=0x07 against known vector + assert.strictEqual( + Buffer.from(d07).toString('hex'), + '5a223ad30b3b8c66a243048cfced430f54e7529287d15150b973133adfac6a2f'); +})().then(common.mustCall()); + +// KT128 with customization string +(async () => { + const digest = await subtle.digest( + { name: 'KT128', outputLength: 256, customization: Buffer.from('test') }, + Buffer.from('hello')); + assert(digest instanceof ArrayBuffer); + assert.strictEqual(digest.byteLength, 32); +})().then(common.mustCall()); + +// TurboSHAKE domain separation out of range +(async () => { + await assert.rejects( + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x00 }, + Buffer.alloc(0)), + { + name: 'OperationError', + }); + await assert.rejects( + subtle.digest( + { name: 'TurboSHAKE128', outputLength: 256, domainSeparation: 0x80 }, + Buffer.alloc(0)), + { + name: 'OperationError', + }); +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encap-decap-ml-kem.js b/test/parallel/test-webcrypto-encap-decap-ml-kem.js index 450ba2cefb0a4f..f3850dcdf02546 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); @@ -40,6 +40,7 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } ['deriveBits'] ); + assert.strictEqual(Object.getPrototypeOf(encapsulated), Object.prototype); assert(encapsulated.sharedKey instanceof CryptoKey); assert(encapsulated.ciphertext instanceof ArrayBuffer); assert.strictEqual(encapsulated.sharedKey.type, 'secret'); @@ -59,6 +60,7 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } ['sign', 'verify'] ); + assert.strictEqual(Object.getPrototypeOf(encapsulated2), Object.prototype); assert(encapsulated2.sharedKey instanceof CryptoKey); assert.strictEqual(encapsulated2.sharedKey.algorithm.name, 'HMAC'); assert.strictEqual(encapsulated2.sharedKey.extractable, false); @@ -93,6 +95,7 @@ async function testEncapsulateBits({ name, publicKeyPem, privateKeyPem, results // Test successful encapsulation const encapsulated = await subtle.encapsulateBits({ name }, publicKey); + assert.strictEqual(Object.getPrototypeOf(encapsulated), Object.prototype); assert(encapsulated.sharedKey instanceof ArrayBuffer); assert(encapsulated.ciphertext instanceof ArrayBuffer); assert.strictEqual(encapsulated.sharedKey.byteLength, 32); // ML-KEM shared secret is 32 bytes @@ -253,12 +256,16 @@ async function testDecapsulateBits({ name, publicKeyPem, privateKeyPem, results (async function() { const variations = []; - vectors.forEach((vector) => { + for (const vector of vectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage(`Skipping unsupported ${vector.name} test`); + continue; + } variations.push(testEncapsulateKey(vector)); variations.push(testEncapsulateBits(vector)); variations.push(testDecapsulateKey(vector)); variations.push(testDecapsulateBits(vector)); - }); + } await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js index 0825027a7c3c02..723fd26ea5708b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-chacha20-poly1305.js @@ -5,9 +5,6 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -if (process.features.openssl_is_boringssl) - common.skip('Skipping unsupported ChaCha20-Poly1305 test case'); - const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -220,6 +217,7 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { // JWK error conditions const jwkTests = [ + [{ kty: 'oct' }, /Invalid keyData/], [{ k: baseJwk.k }, /Invalid keyData/], [{ ...baseJwk, kty: 'RSA' }, /Invalid JWK "kty" Parameter/], [{ ...baseJwk, use: 'sig' }, /Invalid JWK "use" Parameter/], diff --git a/test/parallel/test-webcrypto-export-import-cfrg.js b/test/parallel/test-webcrypto-export-import-cfrg.js index ae203e1005de0a..60d99319691bc4 100644 --- a/test/parallel/test-webcrypto-export-import-cfrg.js +++ b/test/parallel/test-webcrypto-export-import-cfrg.js @@ -351,7 +351,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, publicUsages), - { message: 'JWK "crv" Parameter and algorithm name mismatch' }); + { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -360,7 +360,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'JWK "crv" Parameter and algorithm name mismatch' }); + { message: crv ? 'JWK "crv" Parameter and algorithm name mismatch' : 'Invalid keyData' }); } await assert.rejects( @@ -419,7 +419,7 @@ async function testImportRaw({ name, publicUsages }) { for (const [name, publicUsages, privateUsages] of [ ['Ed25519', ['verify'], ['sign']], - ['X448', [], ['deriveBits']], + ['X25519', [], ['deriveBits']], ]) { assert.rejects(subtle.importKey( 'spki', diff --git a/test/parallel/test-webcrypto-export-import-ec.js b/test/parallel/test-webcrypto-export-import-ec.js index 46a7e9153f2668..2c1740fc620c6b 100644 --- a/test/parallel/test-webcrypto-export-import-ec.js +++ b/test/parallel/test-webcrypto-export-import-ec.js @@ -318,7 +318,7 @@ async function testImportJwk( { name, namedCurve }, extractable, publicUsages), - { message: 'JWK "crv" does not match the requested algorithm' }); + { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -327,7 +327,7 @@ async function testImportJwk( { name, namedCurve }, extractable, privateUsages), - { message: 'JWK "crv" does not match the requested algorithm' }); + { message: crv ? 'JWK "crv" does not match the requested algorithm' : 'Invalid keyData' }); } await assert.rejects( diff --git a/test/parallel/test-webcrypto-export-import-ml-dsa.js b/test/parallel/test-webcrypto-export-import-ml-dsa.js index ceb652955d5929..5cafdfd41b2715 100644 --- a/test/parallel/test-webcrypto-export-import-ml-dsa.js +++ b/test/parallel/test-webcrypto-export-import-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -25,46 +25,18 @@ function toDer(pem) { return Buffer.alloc(Buffer.byteLength(der, 'base64'), der, 'base64'); } -/* eslint-disable @stylistic/js/max-len */ -const keyData = { - 'ML-DSA-44': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-44', 'public'), 'ascii')), - jwk: { - kty: 'AKP', - alg: 'ML-DSA-44', - pub: 'fYmD1Rx_jkoW9KG7Bs_5zyYEiWEZs15tYBxNdKq9NircZnvZBwwwaGbj0UsxJNc4Dyfp2IFAZZPO3rFCSUdpXHPrGRHwIVMzwiwfu2V7V02xoheW4mrkPThA3JRJSmNdsx6YGu37MaeJkIk6AlUexo46JfGrkRXZp_IyZxiL_L2dPrfwx-32j7WFI5sBadp7cDWfNkJjdQwW4puTe5Rw7h16GHb-DMOAKpfeMHujh7IYHuLCU6lVi90j1m8Ru0dxdmeQ1eY1vDnO7fNQKfzOLhpUNnj7BBZ24GTqFc-SN5HDCSCsSGKScTYYBwiSVTdSGG1GNqIiN2FgE4z1Jj6JFVB_OIUnl4sKbb3m8kB0BwtUPbkC0FVokGRUEGt6ba1Pc_IMpB5Gs3g9PFREI_C9o1yVW3NS2PzH_Vk4Tpf0N1K1kzIK_3IqekLfyqXmVDNsOovsS7Sw9TdmdWUNGRmhXFKRkex5VjpMIx7OwBGsYJCc4FhauWdrVtbkvHGggSpsla73ZcA4Vzh7aq47LMv0KS2YLp-DMn7SEohPHGg74118eLLn88yptxwtwt1dBFj8BKUfPrytuN1EIRQy34hwbkBLN9wDqhgn3Z3fvksRvmgN_4ZQ8YjeD-H3OFh5WJ_Rd66wHSl-YFat-_JF4UPcdlkNUbxPvDi5VL909Pe3VlwEZhT5otdtXQX4U3dUfqWKEh2kN0Q2lo8wbf3OMmBOFTfyX0eYa_5088ZnJvvliefn-TCDyc6WlcZrNqwBOF8N8-IN3b_8RPq-RuV8-mK-M83Hi4ElQB7Z44eZMmfUwFrozEG4Wq2K6MwQ_edG4dWeUVMCloTpGDFOtlLQlDoAN4m_sS2Lbwm_3ra29noUcK8_j10yy-hENE2Yluh1pIL-GoWZj3uYO-rEKVbszaagdE0DJ_uQcHUdNnBHKn64-cQ6xihXzxaeHx9OxkWWMKbzLtKpuYDK_X7EVvm8YTjl_oTsr2SWT2usjNJko32DhRV-OXLKKHo5FJpCy2bGFLXGG26CglUvgZQ2dyXiWeGVNKffOv1cQ5R_RlU2MpLiZ1bigy9hh4lu_XAHLfjQfhf71jeMuF4nEBWV-YOAjDTaDB2hcGqv_XcGXcmLWHqOWgc5Mb6lkb2zYs_oyOskmyFx6C0P7UrV8kCiN4zbuTqZNdNjlWL_QJUmU3vk6CpNa0XN1M3sLjZpOEsaqgRVPLcIDH-juVhyWiymuxe-8yNCOFSKxhscew08EQ9DEckP_iIA8qU2gcreHtvAS5VA5Emz1K2ypYe6oS3ogP-CX4nOAEfvjsb1HHJoclgiwjL1BtCLFgOE-0vn1M-nVOE6WbHGHoNKMJMHP2a3HQC7DmDfSOw5P6Cj5X7QVqhCY6tAGZWEPu3hUssp7K5UJePEdBn_LrErt4ucyXW6y1PAA2Fn8EuHaRyf2ggibDGnzq8E15m_R4LMvZAuGR0bN9jBTlm_x4ZQMqFwKkIdllkN1QTErazOyNsgU6fhA_20h5EIYT6-LqXr_Otj3Kp8MkJB9c3XNGoo5sbHTQCt0VNOHoxCFP_swiAJLtm743eOsI1M6naWLIqPagSCioosAvJYowypJQGvM-N3hBu8KUr0f911KRN7WqTAXTOHZ_vvTqcWKet0dFdh1EHuP3TrU8hSMciaphGvuK93T3gaWuJ6lcCkQndWvEo9S6FQB7eLU_ALKOQ3ROybUUkXgfyTkWDPxbHdeJCgMRv6Ig1PShPyxYb4ig', - priv: '273AhMPiZWLlSQCY41yi1fMj6xavGH0btB23zMhI1uY', - }, - }, - 'ML-DSA-65': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-65', 'public'), 'ascii')), - jwk: { - kty: 'AKP', - alg: 'ML-DSA-65', - pub: 'hxPP5LvG83t2fJyfA1TUssJK_ydrzryrCHGZuKFxmnl5Y3sxHRCPW_JpHEoiIgR6kgELnwibZnueax1zFerTOTA7o0NwXHFiaEB-8AmqJI93DkvtbUOSTCixa3admQBKW_PtgMCVtaEVuuvCuOEFhOyuZkyfvnpBwUKOkz3t-O1wpgrSmf-rdPXOEv8YcsSn-xfLYPSLzPCnt7gnIX_fwtkgnXjref-QqjFKlKZE2e7MkmHeViJ4iGy78r3UzVhBHsmFGC0ZNc8-iT3muH5Sn0SXmNq-F2EoerWLIAsPxL2KE6UrqPAwTbHn1B5sAGWvhsVVLlFPI1s1JLVLBNRJ5vhif525xNIpMAMuAZrteD827pve3zQo9_GHjWgykj9VzM9PEcVmVqxZ5u41kUXsM4PWZF29Oh2sYsmJ2LdiJ9RcA91vRLG2DqEYm-V5JwIz8uxL17DUsEC7zYthvtqGASq05CbfPTBev33rQUv4H0Etz99U89WooTk0FisHDz1uEUilU_VY1tN5byIDitXNf0jnz3SIHDUUZARn7ll0YwO0jtksT68sQW3Liy6Exhlp1td0so2qZUrbVZasjyCOVuibwbwvrdpP3QRsoG5UqkAqk8Rm2iCpdQSg87pswOscgA8AC8TczGHNfXc9PqzAmbsEPKvmZuE60HLGzqpRqFULf3nyYUQUqbdmKJsKQ29LXeDVbyy3-fkTUDuYqNC2tBY7PkzHJSA9Z4hDC_BHEFxcelibScSNyf7y4lDVWnuJMXpQ0WRh3UkUPa007IerhixwxvBvFXQR-ytYinixvjirlcEF1wQI1DzE8KjOXYYuFPS4Yl8HeZHQ-64Q0RuxlKIRP3YvjZWh4IDVvEVs5ZLzZPbE3Twe0N5a7iCu0BzZWTeHNbcMoViFyJTpec2w3vVHeI4PJB-5HeI8xuh-9y8ytTau8QtMe4thoROoajizDQLrkw3e6ryJJ3R84i0oni4vmZWyLDilwcLqPOkQJCIDMjq7exdmVX5t3DtAW4F6Coz0z3sf7tGlSMVxA7izCoVbG0y2_l1P2h7fWBuEPT7PWlMdPqu9Pj5jqXY6jJ0nkaR_pp7dDhO1HKae5edcBYunHZqVQQjRZ_DvKzbPrDk5t6Xq9fdSkiAeP3B4qn5uU-nx7OaX7DRoVEnbbiEDynIRPSEY-Ts3alPJtBv8zuzaGNyX05Z9MyZ0w-VlC-WxOBdVEsIAp_4uJ3kQ3UsfE9DLJH8WPuDI4t4i2VnNNyFlI0XSUocc_0rWgqp2I1UzSzkVbklwkuFywPI645u4G2XAlfdd_wpjFGC-IUPXgpeSfspPwW15sBP-ITS-gwtvfzQVLpRS0euzN97xo_GMhNPZ4bW-YyZt8z_R8bsQ8ktfoP-5RUV-yzYDt0tA01QJsZdBLf5J_H7qP8l4c8V4hPe_CFL032obbxmAnVPAP69u2SaMBlL8azjk4wGVFQpQp1JqMJCao2W8ZImCVegkPZxhGbx0nkgVfyFx4ihMeDNM288JbGC4CGON8C02Q84rQzhwzZE83Y9rSe1Bb0fUMHMu6ihD5jLdeltuBL4ZdJlKgL24KZK5o5pq4_l9SyzGAjB1KAQnClNOB1SxV89CtILu-65wb17s0z3qw2-NF0B6UVlGQFebjbSyLQv2ARaETh_8cBiPugVMgBIV3K1KBwNyWejyI1ZDCssvIZHJCF2SRW3HmJerTiB23eGHFYKSLdxW7LEzoHIc2xZEc3pwR43gavjeoL0pNc-HNFV_c19wiH7Tnw3IHld_FfTqAIPnqKMNIY7D_D0DmFNTOdnzcipqKxUB0Avc-wr8Fz0gjeRpLH2iDSCJWtvWjoeYvHTktGsblDAM5j9xznwEvZfQvj8fTUnFxl3clkD6e9V1jrDQDkXfOtl-bDIv9PtMwamfJFu-z2ubF-gKytUewPNo10uhwr2TDNdUayCZDR2T3HoRLN8goIw2bFoPJ98LoPcSukEvKABjH0DiHNeqFELNZPx_uCx5N-YFkUZxHWA1QUoGhqQ3REtcT3c-SZf_TDFOPvws6bmwt4lcWpLmubOAJLFt6J8m8HCkVUshRdFzvHQm_0JEvA3JtyXZzvsPUv5njdk0nxTZktvsnqX054RQk5x8U-lBY-bK3uMOoFnHju45LMoHCUgGJi22eUm7nLGZEh84ZAbNlPLXpfavXvPJh21OW5EOAeuQ-yWNHY2xbmAiHNnb-J2VpZc1Vy82sxn8umFtKduuQuIQMOsf4qHqj5MzDY_1NjrM4Wm7XAiLC4MpQ22w9PWNQXSZWvo2fj8WUnfEibpgyRkoD2P25GRQqsRJ3-Ykl5bm_2Vfe6i3oXHOwQwZwKGXfAqXyo4iU1UI7e-qC4sj5U64oB_A_NSBaJJrZoQ2fVeGTnFxA4QMMoWCT0VlwBXK0B3jht8Xal3WcI-i9ctQB1-GrmwwgG2ttePHt1IKy69bSZE3FLkFicaHg6VxypG6ef8rVsmMrfpTATOnF5_iEaLNY9428HHGW0iz4vXwaE-MkYy7NK2KMPFiCB0ec9OjIROwayK4LREv4qknWHnVQRSm25Rr9DcVFXKj16Au7X1hv7TuVH7h25U', - priv: '1X9VEr_iXMRwBvnSytEmHrtA-DpD6FWAUqMrDNlJVBg', - }, - }, - 'ML-DSA-87': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-dsa-87', 'public'), 'ascii')), - jwk: { - kty: 'AKP', - alg: 'ML-DSA-87', - pub: 'DZXqaBATRN0GRtigxzkLxp7C9fFYxI7Gl-tdfXqJHbzVCTTvRfRwZcu3YmpsUYXBdX2pVsQ51QlqxMslKBRmfNCanBLcfd57qoEIb0K6GIKZGHxlsr9aXNjEGcKMo0ICon0LYTvTWrl72Oz-2yEA_abPK3_dBUFGAYQ6kOQhAHcT1CMmTTck23PnEd5WUpYfZOA9giFX9dNVrrdFWczj_vDOty81ObNKsxfVWT1nG7c60UCJxb2c2tMrBx3rp7Hfc_aOg54W5KHocJi0Eai0ok4buySTe0UCSCUTkeoCcdiABgOFBiXRYzpm3Lz4uot6hSgFpuh67fE9Zpgtn64vfI-O1mgcPrPPpd3yA92Jrq-dvXuM55w1RmA_hha3U5Sh2vm0tD1U57q945UppFReIv_8NAKBkxQ_vHil7ySm-m7IAM-sTUY86_IqMZqisxoz7Ff7ZR2vIiUm3-L0ow4B8uPsCv2ZlUoVXvMF6XQiOHsgqgP1rfH8DmfmPFudwiXrAW6wEmi10skPmkN92aC3TPG6nmaNryQ7f8J82yVmGxW9U7zMbg21qNkRGBi_1YwEt6D8V2pUWv5U1a4p4-Ma0f4uQG4g0odM-WGomlh7pZWZf3sffiPXk9wBrGisxtCJuaB5vtkheWxpEfWqnhc3QdOWfrsRg6P1h7M95SNVW0U8A38wwrqPOpzEnckCVdCrZz2b2KVln6a4twfINg1-3lEZR4rkEmTaTYlLFlXzbRFWBBPGATxeRxhQ_9N5VhHi7STWPFD5HIJyVqz436bbVvM6Py_oldT_xt_tWlPc0w4Pesy2CgaCPlJCnx6cjEg_sRBUcRkBoHqa7aZj4JFFm9bzEaiJ2MKfkHVT4xdbEimMHsD0HkIQpg5-zoB2Jsqgc6Qi3L57hZi-Q1V0G2lmdZ5WZkQ2m5hxle4hHtAmghgynK2p0qzDWHScxHcdd2sInYqQgMYbnvs04YYKWpIfndCUBs9q_EONN5tn8gfSwHEKlQ-KpplEL5kc-99h2uaZsxRlJOF6_z8EZ-aKaKY8jvoAV0g4kZJH5UKy_MkBiva6r-zXUmo88qJjXQatOOSdfvJUTiZiSfcpBQqF9SSDD9WWsgInaOCKO_fAFf9fuacXDMEj0esUx1YrVEe_77S5uObg-UrK405U1JhKJJvd7o8xQKxenv5BJdsbbyYQDbSSe9BrCqeHEgmfRHTXdSvl_3QOP0Ej-dT8YJJHZ1lrujU7Zg5f5Kg99tU5GdLMbHX2kt4F2a0NX09HikEemvUg4NLPhjOihfVkChr-zdF69nfsnTiaQrMgpIcl9jttN99_8Gju-LU8OWbb92m9RLxAUFP115v22f77YPoILm92IjMZMkxEhGneoclWhnudkyR7YoTBjCnT5b7AC9_05uls637FmVf7Ck8-MF4gLil3dstXi4g24bitYhxxqWwiqF4vsDouSGUnuKCMwx3TLsII_xk77TjpQP4vpLdYM3tn94AVlTMMhnI-OZkVJk-_mIbywCwRHlQb5nzVCc0BWlM1kb9PJys2IfciS8LWEoxeq9moDX5w72yJKoLN3CWpD3VdJAiW79zUaySw-IeW0XaHnlze5fYnOozG8lIeyQ9sMZasMiFovGnR3b7jyMtA38U33v16fouWuBILOu0m_QOpRDI9i3rjRM6hdC48zCtNSzc1_1VPYkWDSFK1oVAjdd8-2rjyqdPeUwnqD26VA9_d3R7x8ThrazdbRC8U1hr9jpqNHuZ4LGYu3Ui8wB-lSt9QMaHz517MY_zBEoNGyvbQWtlM7mvLu12KoMM7nvGrPJnvD-HmxTqsVQolD8_lIV5ao72yiKDpArVr6RuV4PpI0j_Wy4-yDCuwBW0gjnB9GvCwOTeByYXJT6Ul7dgHck4BbF3IyFgvmY--ceWr5mBrbAC9LJP_4Wf5O6ul3hFrhiG6zSV4zzBYLnEwfW6LNLEZjZKgmBYiC5s1xlxYWDdcQ5FGmLQ9uEDkr4VItXQWvIIdeBQPyujxmd965Mig9-Sa7SCyV_3wH8fQnGlvU-jJMGL0zvzB2gcu7hMLMagUBj1AKXj-UxpbX1i95f2TOiZDwMeCCszgvCjQ21XKg07TBXrrOiFcgcADgdo-HJr7O1T3ozOIulq1PDM7QZH6i3wDD0j3b0NCdqKCWqhfLXz2-FszyUHmA_GCzOLVzrLT2DcGWIcQbkvF0yZPgTyqKArKa8qytOerdH6oCJ0bRl96855sMVjuUdyVLX7XW_rVTwsOwV0gVAx8SrzovtDFeHRNl7BQKMsyQ1BjWu25jqKJ598vAi3LCZv0kMdiC24qPdgZU4e2aUkco11EnD6nJgdqsVFxufCl4BD_D9g5Wy42fJt4ZgNPAcbUf341KERyReeBEQj-qlPB3IUTIXcJw68GScebhxb0W_tGKMBC6-ip4QfNW7UTxUxVxmCV7h0yRnBBlkuUR1eYQwWRmEPjKd3dLHvgHtr266NQmE1tnKtJlsdPKb0ztrI9vogsENsgGNFQ2tHoeX8vqxcagGznlPVPfc3DlqjBSeTFQaPWvmCQHVKgxkbvffKzFQyFvXEqt6bGGtkwBoRJ_IIwtSeWQ2nFPBe3rlyKrtSnQFIMibJbbYvPVE03Cld9R61r-GGDSQz4aXekLzePEVwnxpe4mWJGco7ctQyE73PekL1uo2g0bRK-KgaE878OiLRBo5T7c633xEf2hMy9532M2GVdTZuoE0LL-wpAh9GmNdvJZc7g2sINvwZi778v2WHcYEKqXvdmrX-Shyh3QkzgIGZrDzM4UlxxUWaXfZ0Z6PNguk7Jafqf3xuUe9Z8zfAJl5c_VA3k8dn7IRg99hRsh-TGBCzqzgjJq4p4XMWP2QuxFTGSgHRe2GSCFzd5-lPjrj76ZyT3MPUxQe_bV2VE-Oys3MT-VkCCM8jFCANXdrfltG6jSiUZ2uJUWNqdNnxPglmmyrgff_m-5CyIRWYXQsIdZGspqdjzb4F6RbBKfL2PQlM6zUfo9JNmE8YQq815Nxkex8vDOrImnew312fZA6rRjr9_uE4lEbw3U7PFlCKBUPvPnsdgedjKYhiS0xU6iS1NDKOvYhcrkCkiU67EmFD4U0-OCv9Kpbb5bIxTJuv405NxJBElAMVI-ya0ns7D4-xUPn05E7PhtGZT0eHwItjT6omThTsTHwB_bQqYfNrjrObO1l1go2hQ-cUadZYsG5l47CB5RlFhANtaC8tiq4KJi48TmEEApB_0VwOI4EmI7SR0oaqx3HRXZfeGevCx2yC9aCYM4HqcyqP2g_1HwsOYzwq4XDEbK5Yl1dtYABxPoo7t8FBq2sSmfrBJWFv_nvreb_DPwbfoSeCy9knqvOktSQRrPMmo-nNGpandBvjmrjSk3EdeziAP7XNre5I-bn_2voDxkzGFtUM-wzlL379ASRGej8FkNWaOyqGP6Anq5PSJ', - priv: 'LZSOlEPbU9S5_mSsMULffTyxZu6qKEOQ1nfEi2NCscg', - } - }, -}; -/* eslint-enable @stylistic/js/max-len */ +const keyData = {}; + +for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + const lcName = name.toLowerCase(); + keyData[name] = { + pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName(lcName, 'private_seed_only'), 'ascii')), + pkcs8: toDer(fixtures.readKey(getKeyFileName(lcName, 'private'), 'ascii')), + pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName(lcName, 'private_priv_only'), 'ascii')), + spki: toDer(fixtures.readKey(getKeyFileName(lcName, 'public'), 'ascii')), + jwk: JSON.parse(fixtures.readKey(`${lcName}.json`)), + }; +} const testVectors = [ { @@ -123,12 +95,23 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + assert.strictEqual(err.cause.code, 'ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED'); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -315,16 +298,18 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'Invalid JWK' }); + { message: 'Invalid keyData' }); + const mismatchedPublicKey = Buffer.from(jwk.pub, 'base64url'); + mismatchedPublicKey[0] ^= 0xff; await assert.rejects( subtle.importKey( 'jwk', - { ...jwk, priv: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' }, // Public vs private mismatch + { ...jwk, pub: mismatchedPublicKey.toString('base64url') }, { name }, extractable, privateUsages), - { message: 'Invalid keyData' }); + { name: 'DataError', message: 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -370,7 +355,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, publicUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); await assert.rejects( subtle.importKey( @@ -379,7 +364,7 @@ async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { name }, extractable, privateUsages), - { message: 'JWK "alg" Parameter and algorithm name mismatch' }); + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); } await assert.rejects( @@ -503,16 +488,20 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { }); })().then(common.mustCall()); -(async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED'); - assert.strictEqual(err.cause.message, 'Failed to get raw seed'); - return true; - }); - } -})().then(common.mustCall()); +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED'); + assert.strictEqual(err.cause.message, 'Failed to get raw seed'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-export-import-ml-kem.js b/test/parallel/test-webcrypto-export-import-ml-kem.js index c33eb2b5993156..31ba739a1e55e0 100644 --- a/test/parallel/test-webcrypto-export-import-ml-kem.js +++ b/test/parallel/test-webcrypto-export-import-ml-kem.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -25,29 +25,18 @@ function toDer(pem) { return Buffer.alloc(Buffer.byteLength(der, 'base64'), der, 'base64'); } -const keyData = { - 'ML-KEM-512': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-512', 'public'), 'ascii')), - pub_len: 800, - }, - 'ML-KEM-768': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-768', 'public'), 'ascii')), - pub_len: 1184, - }, - 'ML-KEM-1024': { - pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private_seed_only'), 'ascii')), - pkcs8: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private'), 'ascii')), - pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'private_priv_only'), 'ascii')), - spki: toDer(fixtures.readKey(getKeyFileName('ml-kem-1024', 'public'), 'ascii')), - pub_len: 1568, - }, -}; +const keyData = {}; + +for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { + const lcName = name.toLowerCase(); + keyData[name] = { + pkcs8_seed_only: toDer(fixtures.readKey(getKeyFileName(lcName, 'private_seed_only'), 'ascii')), + pkcs8: toDer(fixtures.readKey(getKeyFileName(lcName, 'private'), 'ascii')), + pkcs8_priv_only: toDer(fixtures.readKey(getKeyFileName(lcName, 'private_priv_only'), 'ascii')), + spki: toDer(fixtures.readKey(getKeyFileName(lcName, 'public'), 'ascii')), + jwk: JSON.parse(fixtures.readKey(`${lcName}.json`)), + }; +} const testVectors = [ { @@ -106,12 +95,26 @@ async function testImportSpki({ name, publicUsages }, extractable) { } async function testImportPkcs8({ name, privateUsages }, extractable) { - const key = await subtle.importKey( - 'pkcs8', - keyData[name].pkcs8, - { name }, - extractable, - privateUsages); + let key; + try { + key = await subtle.importKey( + 'pkcs8', + keyData[name].pkcs8, + { name }, + extractable, + privateUsages); + } catch (err) { + if (process.features.openssl_is_boringssl) { + assert.strictEqual(err.name, 'DataError'); + // It should really only be ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED + // but BoringSSL is inconsistent between handling ML-KEM and ML-DSA + // Fixed in https://github.com/google/boringssl/commit/94c4c7f9e0eeeff72ea1ac6abf1aed5bd2a82c0c + assert.match(err.cause.code, /ERR_OSSL_EVP_UNSUPPORTED_ALGORITHM|ERR_OSSL_EVP_PRIVATE_KEY_WAS_NOT_SEED/); + common.printSkipMessage('Skipping unsupported private key format test'); + return; + } + throw err; + } assert.strictEqual(key.type, 'private'); assert.strictEqual(key.extractable, extractable); assert.deepStrictEqual(key.usages, privateUsages); @@ -209,7 +212,8 @@ async function testImportPkcs8MismatchedSeed({ name, privateUsages }, extractabl } async function testImportRawPublic({ name, publicUsages }, extractable) { - const pub = keyData[name].spki.subarray(-keyData[name].pub_len); + const jwk = keyData[name].jwk; + const pub = Buffer.from(jwk.pub, 'base64url'); const publicKey = await subtle.importKey( 'raw-public', @@ -246,13 +250,14 @@ async function testImportRawPublic({ name, publicUsages }, extractable) { subtle.importKey( 'raw-public', pub, - { name: name === 'ML-KEM-512' ? 'ML-KEM-768' : 'ML-KEM-512' }, + { name: name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768' }, extractable, publicUsages), { message: 'Invalid keyData' }); } async function testImportRawSeed({ name, privateUsages }, extractable) { - const seed = keyData[name].pkcs8_seed_only.subarray(-64); + const jwk = keyData[name].jwk; + const seed = Buffer.from(jwk.priv, 'base64url'); const privateKey = await subtle.importKey( 'raw-seed', @@ -282,15 +287,187 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { { message: 'Invalid keyData' }); } +async function testImportJwk({ name, publicUsages, privateUsages }, extractable) { + + const jwk = keyData[name].jwk; + + const tests = [ + subtle.importKey( + 'jwk', + { + kty: jwk.kty, + alg: jwk.alg, + pub: jwk.pub, + }, + { name }, + extractable, publicUsages), + subtle.importKey( + 'jwk', + jwk, + { name }, + extractable, + privateUsages), + ]; + + const [ + publicKey, + privateKey, + ] = await Promise.all(tests); + + assert.strictEqual(publicKey.type, 'public'); + assert.strictEqual(privateKey.type, 'private'); + assert.strictEqual(publicKey.extractable, extractable); + assert.strictEqual(privateKey.extractable, extractable); + assert.deepStrictEqual(publicKey.usages, publicUsages); + assert.deepStrictEqual(privateKey.usages, privateUsages); + assert.strictEqual(publicKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm.name, name); + assert.strictEqual(privateKey.algorithm, privateKey.algorithm); + assert.strictEqual(privateKey.usages, privateKey.usages); + assert.strictEqual(publicKey.algorithm, publicKey.algorithm); + assert.strictEqual(publicKey.usages, publicKey.usages); + + if (extractable) { + // Test the round trip + const [ + pubJwk, + pvtJwk, + ] = await Promise.all([ + subtle.exportKey('jwk', publicKey), + subtle.exportKey('jwk', privateKey), + ]); + + assert.deepStrictEqual(pubJwk.key_ops, publicUsages); + assert.strictEqual(pubJwk.ext, true); + assert.strictEqual(pubJwk.kty, 'AKP'); + assert.strictEqual(pubJwk.pub, jwk.pub); + + assert.deepStrictEqual(pvtJwk.key_ops, privateUsages); + assert.strictEqual(pvtJwk.ext, true); + assert.strictEqual(pvtJwk.kty, 'AKP'); + assert.strictEqual(pvtJwk.pub, jwk.pub); + assert.strictEqual(pvtJwk.priv, jwk.priv); + + assert.strictEqual(pubJwk.alg, jwk.alg); + assert.strictEqual(pvtJwk.alg, jwk.alg); + } else { + await assert.rejects( + subtle.exportKey('jwk', publicKey), { + message: /key is not extractable/, + }); + await assert.rejects( + subtle.exportKey('jwk', privateKey), { + message: /key is not extractable/, + }); + } + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, use: 'sig' }, + { name }, + extractable, + privateUsages), + { message: 'Invalid JWK "use" Parameter' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, pub: undefined }, + { name }, + extractable, + privateUsages), + { message: 'Invalid keyData' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, kty: 'OKP' }, + { name }, + extractable, + privateUsages), + { message: 'Invalid JWK "kty" Parameter' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk }, + { name }, + extractable, + publicUsages), // Invalid for a private key + { message: /Unsupported key usage/ }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, ext: false }, + { name }, + true, + privateUsages), + { message: 'JWK "ext" Parameter and extractable mismatch' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, priv: undefined }, + { name }, + extractable, + privateUsages), // Invalid for a public key + { message: /Unsupported key usage/ }); + + for (const alg of [undefined, name === 'ML-KEM-768' ? 'ML-KEM-1024' : 'ML-KEM-768']) { + await assert.rejects( + subtle.importKey( + 'jwk', + { kty: jwk.kty, pub: jwk.pub, alg }, + { name }, + extractable, + publicUsages), + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk, alg }, + { name }, + extractable, + privateUsages), + { message: alg ? 'JWK "alg" Parameter and algorithm name mismatch' : 'Invalid keyData' }); + } + + await assert.rejects( + subtle.importKey( + 'jwk', + { ...jwk }, + { name }, + extractable, + [/* empty usages */]), + { name: 'SyntaxError', message: 'Usages cannot be empty when importing a private key.' }); + + await assert.rejects( + subtle.importKey( + 'jwk', + { kty: jwk.kty, /* missing pub */ alg: jwk.alg }, + { name }, + extractable, + publicUsages), + { name: 'DataError', message: 'Invalid keyData' }); +} + (async function() { const tests = []; for (const vector of testVectors) { + if (process.features.openssl_is_boringssl && vector.name === 'ML-KEM-512') { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + continue; + } for (const extractable of [true, false]) { tests.push(testImportSpki(vector, extractable)); tests.push(testImportPkcs8(vector, extractable)); tests.push(testImportPkcs8SeedOnly(vector, extractable)); tests.push(testImportPkcs8PrivOnly(vector, extractable)); tests.push(testImportPkcs8MismatchedSeed(vector, extractable)); + tests.push(testImportJwk(vector, extractable)); tests.push(testImportRawSeed(vector, extractable)); tests.push(testImportRawPublic(vector, extractable)); } @@ -299,24 +476,57 @@ async function testImportRawSeed({ name, privateUsages }, extractable) { })().then(common.mustCall()); (async function() { - const alg = 'ML-KEM-512'; - const pub = keyData[alg].spki.subarray(-keyData[alg].pub_len); + const alg = 'ML-KEM-768'; + const pub = Buffer.from(keyData[alg].jwk.pub, 'base64url'); await assert.rejects(subtle.importKey('raw', pub, alg, false, []), { name: 'NotSupportedError', - message: 'Unable to import ML-KEM-512 using raw format', + message: 'Unable to import ML-KEM-768 using raw format', }); })().then(common.mustCall()); +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED'); + assert.strictEqual(err.cause.message, 'Failed to get raw seed'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} + +// Regression test: JWK `key_ops` validation must recognize ML-KEM operations +// (encapsulateKey, encapsulateBits, decapsulateKey, decapsulateBits) so that +// duplicate entries are rejected (async function() { - for (const { name, privateUsages } of testVectors) { - const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); - const keyObject = createPrivateKey(pem); - const key = keyObject.toCryptoKey({ name }, true, privateUsages); - await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { - assert.strictEqual(err.name, 'OperationError'); - assert.strictEqual(err.cause.code, 'ERR_CRYPTO_OPERATION_FAILED'); - assert.strictEqual(err.cause.message, 'Failed to get raw seed'); - return true; - }); + for (const op of ['encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits']) { + const jwk = { ...keyData['ML-KEM-768'].jwk, key_ops: [op, op] }; + await assert.rejects( + subtle.importKey('jwk', jwk, { name: 'ML-KEM-768' }, true, [op]), + { name: 'DataError', message: /Duplicate key operation/ }); } })().then(common.mustCall()); + +if (!process.features.openssl_is_boringssl) { + (async function() { + for (const { name, privateUsages } of testVectors) { + const pem = fixtures.readKey(getKeyFileName(name.toLowerCase(), 'private_priv_only'), 'ascii'); + const keyObject = createPrivateKey(pem); + const key = keyObject.toCryptoKey({ name }, true, privateUsages); + await assert.rejects(subtle.exportKey('pkcs8', key), (err) => { + assert.strictEqual(err.name, 'OperationError'); + return true; + }); + } + })().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported private key format test'); +} diff --git a/test/parallel/test-webcrypto-export-import-rsa.js b/test/parallel/test-webcrypto-export-import-rsa.js index d3af8ec6c3adb9..9eb611533a7740 100644 --- a/test/parallel/test-webcrypto-export-import-rsa.js +++ b/test/parallel/test-webcrypto-export-import-rsa.js @@ -596,6 +596,20 @@ async function testImportJwk( extractable, publicUsages), { name: 'DataError', message: 'Invalid keyData' }); + + for (const field of ['p', 'q', 'dp', 'dq', 'qi']) { + const jwkMissingCrtField = { ...jwk }; + delete jwkMissingCrtField[field]; + await assert.rejects( + subtle.importKey( + 'jwk', + jwkMissingCrtField, + { name, hash }, + extractable, + privateUsages), + { name: 'DataError', message: 'Invalid keyData' }, + `missing private JWK CRT field ${field}`); + } } // combinations to test diff --git a/test/parallel/test-webcrypto-internal-slots.mjs b/test/parallel/test-webcrypto-internal-slots.mjs index 9b824167ba4553..935cd732bacb30 100644 --- a/test/parallel/test-webcrypto-internal-slots.mjs +++ b/test/parallel/test-webcrypto-internal-slots.mjs @@ -18,4 +18,11 @@ assert.ok(kp.publicKey.usages.includes('foo')); assert.ok(util.inspect(kp.publicKey).includes("algorithm: { name: 'Ed25519' }")); assert.ok(util.inspect(kp.publicKey).includes("usages: [ 'verify' ]")); +const jwk = await subtle.exportKey('jwk', kp.publicKey); +assert.deepStrictEqual(jwk.key_ops, ['verify']); +jwk.key_ops.push('foo'); +assert.ok(!util.inspect(kp.publicKey).includes('foo')); +assert.deepStrictEqual((await subtle.exportKey('jwk', kp.publicKey)).key_ops, + ['verify']); + await subtle.sign('Ed25519', kp.privateKey, Buffer.alloc(32)); diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index e57c34436578ab..989fdbb476162a 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -135,6 +135,23 @@ const vectors = { 'deriveBits', ], }, + 'AES-KW': { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'wrapKey', + 'unwrapKey', + ], + }, + 'ChaCha20-Poly1305': { + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }, }; if (!process.features.openssl_is_boringssl) { @@ -152,23 +169,6 @@ if (!process.features.openssl_is_boringssl) { 'deriveBits', ], }; - vectors['AES-KW'] = { - algorithm: { length: 256 }, - result: 'CryptoKey', - usages: [ - 'wrapKey', - 'unwrapKey', - ], - }; - vectors['ChaCha20-Poly1305'] = { - result: 'CryptoKey', - usages: [ - 'encrypt', - 'decrypt', - 'wrapKey', - 'unwrapKey', - ], - }; } else { common.printSkipMessage('Skipping unsupported test cases'); } @@ -196,7 +196,7 @@ if (hasOpenSSL(3)) { } } -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { result: 'CryptoKeyPair', @@ -297,6 +297,17 @@ if (hasOpenSSL(3, 5)) { Promise.all(tests).then(common.mustCall()); } +// Test CryptoKeyPair prototype +{ + subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify']) + .then(common.mustCall((pair) => { + assert.strictEqual(Object.getPrototypeOf(pair), Object.prototype); + })); +} + // Test RSA key generation { async function test( @@ -606,17 +617,10 @@ if (hasOpenSSL(3, 5)) { [ 'AES-CBC', 256, ['encrypt', 'decrypt']], [ 'AES-GCM', 128, ['encrypt', 'decrypt']], [ 'AES-GCM', 256, ['encrypt', 'decrypt']], + [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], + [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], ]; - if (!process.features.openssl_is_boringssl) { - kTests.push( - [ 'AES-KW', 128, ['wrapKey', 'unwrapKey']], - [ 'AES-KW', 256, ['wrapKey', 'unwrapKey']], - ); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); - } - const tests = Promise.all(kTests.map((args) => test(...args))); tests.then(common.mustCall()); @@ -772,7 +776,7 @@ assert.throws(() => new CryptoKey(), { code: 'ERR_ILLEGAL_CONSTRUCTOR' }); } // Test ML-DSA Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -815,7 +819,7 @@ if (hasOpenSSL(3, 5)) { } // Test ML-KEM Key Generation -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test( name, privateUsages, @@ -850,7 +854,13 @@ if (hasOpenSSL(3, 5)) { assert.strictEqual(publicKey.usages, publicKey.usages); } - const kTests = ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']; + const kTests = ['ML-KEM-768', 'ML-KEM-1024']; + + if (!process.features.openssl_is_boringssl) { + kTests.unshift('ML-KEM-512'); + } else { + common.printSkipMessage('Skipping unsupported ML-KEM-512 test'); + } const tests = kTests.map((name) => test(name, ['decapsulateKey', 'decapsulateBits'], diff --git a/test/parallel/test-webcrypto-methods-not-async.js b/test/parallel/test-webcrypto-methods-not-async.js new file mode 100644 index 00000000000000..c3507b0c103580 --- /dev/null +++ b/test/parallel/test-webcrypto-methods-not-async.js @@ -0,0 +1,43 @@ +'use strict'; + +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +const AsyncFunction = async function() {}.constructor; + +const methods = [ + 'decrypt', + 'decapsulateBits', + 'decapsulateKey', + 'deriveBits', + 'deriveKey', + 'digest', + 'encapsulateBits', + 'encapsulateKey', + 'encrypt', + 'exportKey', + 'generateKey', + 'getPublicKey', + 'importKey', + 'sign', + 'unwrapKey', + 'verify', + 'wrapKey', +]; + +(async function() { + for (const name of methods) { + assert.notStrictEqual(subtle[name].constructor, AsyncFunction); + + const promise = subtle[name].call({}); + assert.strictEqual(Object.getPrototypeOf(promise), Promise.prototype); + await assert.rejects(promise, { + code: 'ERR_INVALID_THIS', + }); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs index b4fbedba5e3242..17cc5c97716df0 100644 --- a/test/parallel/test-webcrypto-promise-prototype-pollution.mjs +++ b/test/parallel/test-webcrypto-promise-prototype-pollution.mjs @@ -1,9 +1,10 @@ import * as common from '../common/index.mjs'; +import assert from 'node:assert'; if (!common.hasCrypto) common.skip('missing crypto'); // WebCrypto subtle methods must not leak intermediate values -// through Promise.prototype.then pollution. +// through Promise.prototype.then or constructor pollution. // Regression test for https://github.com/nodejs/node/pull/61492 // and https://github.com/nodejs/node/issues/59699. @@ -13,51 +14,442 @@ const { subtle } = globalThis.crypto; Promise.prototype.then = common.mustNotCall('Promise.prototype.then'); -await subtle.digest('SHA-256', new Uint8Array([1, 2, 3])); +// WebCrypto methods return native promises. Re-wrapping a promise with +// PromiseResolve() or chaining it with Promise.prototype.then can read +// user-mutated constructor/species accessors. +async function assertNoPromiseConstructorAccess(name, fn) { + const constructorDescriptor = + Object.getOwnPropertyDescriptor(Promise.prototype, 'constructor'); + const speciesDescriptor = + Object.getOwnPropertyDescriptor(Promise, Symbol.species); + let promise; + Object.defineProperty(Promise.prototype, 'constructor', { + __proto__: null, + configurable: true, + get: common.mustNotCall( + `${name} Promise.prototype.constructor getter`), + }); + Object.defineProperty(Promise, Symbol.species, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} Promise[Symbol.species] getter`), + }); + try { + promise = fn(); + } finally { + Object.defineProperty( + Promise.prototype, + 'constructor', + constructorDescriptor); + Object.defineProperty(Promise, Symbol.species, speciesDescriptor); + } + return await promise; +} -await subtle.generateKey({ name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +// Exercise each export format through the same promise-constructor guard. +function assertExportKeyNoPromiseConstructorAccess(name, format, key) { + return assertNoPromiseConstructorAccess(`exportKey ${name}`, () => + subtle.exportKey(format, key)); +} -await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +// Non-promise object results must be fulfilled without thenable assimilation +// observing inherited then accessors on the returned object. +async function assertNoInheritedThenAccess(name, prototype, prototypeName, fn) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, 'then'); + Object.defineProperty(prototype, 'then', { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} ${prototypeName}.prototype.then`), + }); + try { + return await fn(); + } finally { + if (descriptor === undefined) { + delete prototype.then; + } else { + Object.defineProperty(prototype, 'then', descriptor); + } + } +} -const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); +function assertNoInheritedArrayBufferThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + ArrayBuffer.prototype, + 'ArrayBuffer', + fn); +} + +function assertNoInheritedCryptoKeyThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + CryptoKey.prototype, + 'CryptoKey', + fn); +} + +function assertNoInheritedObjectThenAccess(name, fn) { + return assertNoInheritedThenAccess( + name, + Object.prototype, + 'Object', + fn); +} + +// wrapKey('jwk') stringifies an internally exported JWK. The spec does this +// in a fresh global, so inherited toJSON hooks from the current global must +// not observe or replace key material. +async function assertNoInheritedToJSONAccess(name, fn) { + const objectDescriptor = + Object.getOwnPropertyDescriptor(Object.prototype, 'toJSON'); + const arrayDescriptor = + Object.getOwnPropertyDescriptor(Array.prototype, 'toJSON'); + Object.defineProperty(Object.prototype, 'toJSON', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Object.prototype.toJSON`), + }); + Object.defineProperty(Array.prototype, 'toJSON', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Array.prototype.toJSON`), + }); + try { + return await fn(); + } finally { + if (objectDescriptor === undefined) { + delete Object.prototype.toJSON; + } else { + Object.defineProperty(Object.prototype, 'toJSON', objectDescriptor); + } + if (arrayDescriptor === undefined) { + delete Array.prototype.toJSON; + } else { + Object.defineProperty(Array.prototype, 'toJSON', arrayDescriptor); + } + } +} + +// JWK export creates and fills a result object. The exported members must be +// own data properties, not writes that can observe inherited accessors. +async function assertNoInheritedJwkPropertyAccess(name, fn) { + const properties = [ + 'alg', + 'crv', + 'd', + 'dp', + 'dq', + 'e', + 'ext', + 'k', + 'key_ops', + 'kty', + 'n', + 'p', + 'priv', + 'pub', + 'q', + 'qi', + 'x', + 'y', + ]; + const descriptors = new Map(); + for (const property of properties) { + descriptors.set( + property, + Object.getOwnPropertyDescriptor(Object.prototype, property)); + Object.defineProperty(Object.prototype, property, { + __proto__: null, + configurable: true, + get: common.mustNotCall(`${name} Object.prototype.${property} getter`), + set: common.mustNotCall(`${name} Object.prototype.${property} setter`), + }); + } + try { + return await fn(); + } finally { + for (const property of properties) { + const descriptor = descriptors.get(property); + if (descriptor === undefined) { + delete Object.prototype[property]; + } else { + Object.defineProperty(Object.prototype, property, descriptor); + } + } + } +} + +// unwrapKey('jwk') parses a JWK and then converts it to the JsonWebKey IDL +// dictionary. The parsed JWK must provide its own kty member; an inherited +// Object.prototype.kty must not satisfy that required WebCrypto step. +async function assertMissingJwkKtyIgnoresPrototype(fn) { + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, 'kty'); + Object.defineProperty(Object.prototype, 'kty', { + __proto__: null, + configurable: true, + value: 'oct', + }); + try { + await assert.rejects(fn(), { name: 'DataError' }); + } finally { + if (descriptor === undefined) { + delete Object.prototype.kty; + } else { + Object.defineProperty(Object.prototype, 'kty', descriptor); + } + } +} + +// wrapKey('jwk') UTF-8 encodes the JSON string. That step must not rely on +// user-mutable encoding APIs such as TextEncoder or Buffer. +async function assertNoUserMutableEncodeAccess(name, fn) { + const textEncoderDescriptor = + Object.getOwnPropertyDescriptor(TextEncoder.prototype, 'encode'); + const bufferFromDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'from'); + Object.defineProperty(TextEncoder.prototype, 'encode', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} TextEncoder.prototype.encode`), + }); + Object.defineProperty(Buffer, 'from', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.from`), + }); + try { + return await fn(); + } finally { + Object.defineProperty( + TextEncoder.prototype, + 'encode', + textEncoderDescriptor); + Object.defineProperty(Buffer, 'from', bufferFromDescriptor); + } +} + +// unwrapKey('jwk') decodes the wrapped bytes as UTF-8. That step must not +// rely on user-mutable encoding APIs such as TextDecoder or Buffer. +async function assertNoUserMutableDecodeAccess(name, fn) { + const textDecoderDescriptor = + Object.getOwnPropertyDescriptor(TextDecoder.prototype, 'decode'); + const bufferFromDescriptor = Object.getOwnPropertyDescriptor(Buffer, 'from'); + const bufferToStringDescriptor = + Object.getOwnPropertyDescriptor(Buffer.prototype, 'toString'); + Object.defineProperty(TextDecoder.prototype, 'decode', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} TextDecoder.prototype.decode`), + }); + Object.defineProperty(Buffer, 'from', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.from`), + }); + Object.defineProperty(Buffer.prototype, 'toString', { + __proto__: null, + configurable: true, + value: common.mustNotCall(`${name} Buffer.prototype.toString`), + }); + try { + return await fn(); + } finally { + Object.defineProperty( + TextDecoder.prototype, + 'decode', + textDecoderDescriptor); + Object.defineProperty(Buffer, 'from', bufferFromDescriptor); + Object.defineProperty( + Buffer.prototype, + 'toString', + bufferToStringDescriptor); + } +} + +// encapsulateKey() first resolves an internal encapsulateBits job whose result +// object contains a raw sharedKey. The final method result is also an object, +// but its sharedKey is a CryptoKey and is intentionally returned to the caller. +async function assertNoRawSharedKeyObjectThenAccess(name, fn) { + const descriptor = Object.getOwnPropertyDescriptor(Object.prototype, 'then'); + Object.defineProperty(Object.prototype, 'then', { + __proto__: null, + configurable: true, + get() { + if (Object.hasOwn(this, 'sharedKey') && + this.sharedKey instanceof ArrayBuffer) { + assert.fail(`${name} Object.prototype.then observed raw sharedKey`); + } + return undefined; + }, + }); + try { + return await fn(); + } finally { + if (descriptor === undefined) { + delete Object.prototype.then; + } else { + Object.defineProperty(Object.prototype, 'then', descriptor); + } + } +} + +await assertNoPromiseConstructorAccess('digest', () => + subtle.digest('SHA-256', new Uint8Array([1, 2, 3]))); -const importedKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, false, ['encrypt', 'decrypt']); +const secretKey = await assertNoPromiseConstructorAccess( + 'generateKey secret', + () => subtle.generateKey( + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); -const exportableKey = await subtle.importKey( - 'raw', rawKey, { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); +const extractableKeyPair = await assertNoPromiseConstructorAccess('generateKey pair', () => + subtle.generateKey( + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign', 'verify'])); + +const rawKey = globalThis.crypto.getRandomValues(new Uint8Array(32)); -await subtle.exportKey('raw', exportableKey); +const importedKey = await assertNoPromiseConstructorAccess('importKey', () => + subtle.importKey( + 'raw', + rawKey, + { name: 'AES-CBC', length: 256 }, + false, + ['encrypt', 'decrypt'])); + +await assertNoInheritedCryptoKeyThenAccess('importKey', () => + subtle.importKey( + 'raw', + rawKey, + { name: 'AES-CBC', length: 256 }, + false, + ['encrypt', 'decrypt'])); + +await assertNoInheritedJwkPropertyAccess('exportKey jwk secret', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk secret', + 'jwk', + secretKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk secret', () => + subtle.exportKey('jwk', secretKey)); +await assertNoInheritedJwkPropertyAccess('exportKey jwk public', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk public', + 'jwk', + extractableKeyPair.publicKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk public', () => + subtle.exportKey('jwk', extractableKeyPair.publicKey)); +await assertNoInheritedJwkPropertyAccess('exportKey jwk private', () => + assertExportKeyNoPromiseConstructorAccess( + 'jwk private', + 'jwk', + extractableKeyPair.privateKey)); +await assertNoInheritedObjectThenAccess('exportKey jwk private', () => + subtle.exportKey('jwk', extractableKeyPair.privateKey)); +await assertNoInheritedArrayBufferThenAccess('exportKey raw secret', () => + subtle.exportKey('raw', secretKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'raw secret', + 'raw', + secretKey); +await assertNoInheritedArrayBufferThenAccess('exportKey spki', () => + subtle.exportKey('spki', extractableKeyPair.publicKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'spki', + 'spki', + extractableKeyPair.publicKey); +await assertNoInheritedArrayBufferThenAccess('exportKey pkcs8', () => + subtle.exportKey('pkcs8', extractableKeyPair.privateKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'pkcs8', + 'pkcs8', + extractableKeyPair.privateKey); +await assertNoInheritedArrayBufferThenAccess('exportKey raw-public', () => + subtle.exportKey('raw-public', extractableKeyPair.publicKey)); +await assertExportKeyNoPromiseConstructorAccess( + 'raw-public', + 'raw-public', + extractableKeyPair.publicKey); const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); const plaintext = new TextEncoder().encode('Hello, world!'); -const ciphertext = await subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext); +const ciphertext = await assertNoPromiseConstructorAccess('encrypt', () => + subtle.encrypt({ name: 'AES-CBC', iv }, importedKey, plaintext)); -await subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext); +await assertNoPromiseConstructorAccess('decrypt', () => + subtle.decrypt({ name: 'AES-CBC', iv }, importedKey, ciphertext)); const signingKey = await subtle.generateKey( - { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']); + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign', 'verify']); const data = new TextEncoder().encode('test data'); -const signature = await subtle.sign('HMAC', signingKey, data); +const signature = await assertNoPromiseConstructorAccess('sign', () => + subtle.sign('HMAC', signingKey, data)); -await subtle.verify('HMAC', signingKey, signature, data); +await assertNoPromiseConstructorAccess('verify', () => + subtle.verify('HMAC', signingKey, signature, data)); const pbkdf2Key = await subtle.importKey( 'raw', rawKey, 'PBKDF2', false, ['deriveBits', 'deriveKey']); -await subtle.deriveBits( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, 256); - -await subtle.deriveKey( - { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, - pbkdf2Key, - { name: 'AES-CBC', length: 256 }, - true, - ['encrypt', 'decrypt']); +await assertNoPromiseConstructorAccess('deriveBits', () => + subtle.deriveBits( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + 256)); + +await assertNoPromiseConstructorAccess('deriveBits PBKDF2 zero-length', () => + subtle.deriveBits( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + 0)); + +const hkdfKey = await subtle.importKey( + 'raw', rawKey, 'HKDF', false, ['deriveBits']); + +await assertNoPromiseConstructorAccess('deriveBits HKDF zero-length', () => + subtle.deriveBits( + { name: 'HKDF', hash: 'SHA-256', salt: rawKey, info: rawKey }, + hkdfKey, + 0)); + +const ecdhKeyPair = await subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-256' }, + false, + ['deriveBits']); + +await assertNoPromiseConstructorAccess('deriveBits ECDH', () => + subtle.deriveBits( + { name: 'ECDH', public: ecdhKeyPair.publicKey }, + ecdhKeyPair.privateKey, + 256)); + +await assertNoPromiseConstructorAccess('deriveKey', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedArrayBufferThenAccess('deriveKey', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('deriveKey result', () => + subtle.deriveKey( + { name: 'PBKDF2', salt: rawKey, iterations: 1000, hash: 'SHA-256' }, + pbkdf2Key, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); const wrappingKey = await subtle.generateKey( { name: 'AES-KW', length: 256 }, true, ['wrapKey', 'unwrapKey']); @@ -65,31 +457,224 @@ const wrappingKey = await subtle.generateKey( const keyToWrap = await subtle.generateKey( { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); -const wrapped = await subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW'); - -await subtle.unwrapKey( - 'raw', wrapped, wrappingKey, 'AES-KW', - { name: 'AES-CBC', length: 256 }, true, ['encrypt', 'decrypt']); - -const { privateKey } = await subtle.generateKey( - { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify']); +const wrapped = await assertNoPromiseConstructorAccess('wrapKey', () => + subtle.wrapKey('raw', keyToWrap, wrappingKey, 'AES-KW')); + +const wrappedJwk = await assertNoInheritedJwkPropertyAccess('wrapKey jwk', () => + assertNoInheritedToJSONAccess('wrapKey jwk', () => + assertNoUserMutableEncodeAccess('wrapKey jwk', () => + assertNoPromiseConstructorAccess('wrapKey jwk', () => + subtle.wrapKey('jwk', keyToWrap, wrappingKey, 'AES-KW'))))); + +await assertNoPromiseConstructorAccess('unwrapKey', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedArrayBufferThenAccess('unwrapKey', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('unwrapKey result', () => + subtle.unwrapKey( + 'raw', + wrapped, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); + +await assertNoUserMutableDecodeAccess('unwrapKey jwk', () => + assertNoPromiseConstructorAccess('unwrapKey jwk', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt']))); +await assertNoInheritedArrayBufferThenAccess('unwrapKey jwk', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); +await assertNoInheritedCryptoKeyThenAccess('unwrapKey jwk result', () => + subtle.unwrapKey( + 'jwk', + wrappedJwk, + wrappingKey, + 'AES-KW', + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt', 'decrypt'])); + +{ + const jwkUnwrappingKey = await subtle.generateKey( + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt', 'unwrapKey']); + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const validWrappedJwk = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + Buffer.from('{"kty":"oct","k":"AAAAAAAAAAAAAAAAAAAAAA"}')); + + await assertNoUserMutableDecodeAccess('unwrapKey jwk AES-CBC', () => + assertNoPromiseConstructorAccess('unwrapKey jwk AES-CBC', () => + subtle.unwrapKey( + 'jwk', + validWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt']))); + await assertNoInheritedCryptoKeyThenAccess( + 'unwrapKey jwk AES-CBC result', + () => subtle.unwrapKey( + 'jwk', + validWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt'])); + } + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const wrappedRawKey = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + rawKey); + + await assertNoPromiseConstructorAccess('unwrapKey raw AES-CBC', () => + subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + await assertNoInheritedArrayBufferThenAccess('unwrapKey raw AES-CBC', () => + subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + await assertNoInheritedCryptoKeyThenAccess( + 'unwrapKey raw AES-CBC result', + () => subtle.unwrapKey( + 'raw', + wrappedRawKey, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 256 }, + true, + ['encrypt'])); + } + + { + const iv = globalThis.crypto.getRandomValues(new Uint8Array(16)); + const missingKtyWrappedJwk = await subtle.encrypt( + { name: 'AES-CBC', iv }, + jwkUnwrappingKey, + Buffer.from('{"k":"AAAAAAAAAAAAAAAAAAAAAA"}')); + + await assertMissingJwkKtyIgnoresPrototype(() => + subtle.unwrapKey( + 'jwk', + missingKtyWrappedJwk, + jwkUnwrappingKey, + { name: 'AES-CBC', iv }, + { name: 'AES-CBC', length: 128 }, + true, + ['encrypt'])); + } +} -await subtle.getPublicKey(privateKey, ['verify']); +await assertNoPromiseConstructorAccess('getPublicKey', () => + subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); +await assertNoInheritedCryptoKeyThenAccess('getPublicKey', () => + subtle.getPublicKey(extractableKeyPair.privateKey, ['verify'])); -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { const kemPair = await subtle.generateKey( - { name: 'ML-KEM-768' }, false, + { name: 'ML-KEM-768' }, true, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits']); - const { ciphertext: ct1 } = await subtle.encapsulateKey( - { name: 'ML-KEM-768' }, kemPair.publicKey, 'HKDF', false, ['deriveBits']); - - await subtle.decapsulateKey( - { name: 'ML-KEM-768' }, kemPair.privateKey, ct1, 'HKDF', false, ['deriveBits']); - - const { ciphertext: ct2 } = await subtle.encapsulateBits( - { name: 'ML-KEM-768' }, kemPair.publicKey); - - await subtle.decapsulateBits( - { name: 'ML-KEM-768' }, kemPair.privateKey, ct2); + await assertNoInheritedArrayBufferThenAccess('exportKey raw-seed', () => + subtle.exportKey('raw-seed', kemPair.privateKey)); + await assertExportKeyNoPromiseConstructorAccess( + 'raw-seed', + 'raw-seed', + kemPair.privateKey); + + const { ciphertext: ct1 } = + await assertNoRawSharedKeyObjectThenAccess('encapsulateKey', () => + assertNoPromiseConstructorAccess('encapsulateKey', () => + subtle.encapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.publicKey, + 'HKDF', + false, + ['deriveBits']))); + + await assertNoPromiseConstructorAccess('decapsulateKey', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + await assertNoInheritedArrayBufferThenAccess('decapsulateKey', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + await assertNoInheritedCryptoKeyThenAccess('decapsulateKey result', () => + subtle.decapsulateKey( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct1, + 'HKDF', + false, + ['deriveBits'])); + + const { ciphertext: ct2 } = + await assertNoPromiseConstructorAccess('encapsulateBits', () => + subtle.encapsulateBits( + { name: 'ML-KEM-768' }, + kemPair.publicKey)); + + await assertNoPromiseConstructorAccess('decapsulateBits', () => + subtle.decapsulateBits( + { name: 'ML-KEM-768' }, + kemPair.privateKey, + ct2)); } diff --git a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js index 1ed74c2508f438..b11e65ade79185 100644 --- a/test/parallel/test-webcrypto-sign-verify-ml-dsa.js +++ b/test/parallel/test-webcrypto-sign-verify-ml-dsa.js @@ -7,8 +7,8 @@ if (!common.hasCrypto) const { hasOpenSSL } = require('../common/crypto'); -if (!hasOpenSSL(3, 5)) - common.skip('requires OpenSSL >= 3.5'); +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) + common.skip('requires OpenSSL >= 3.5 or BoringSSL'); const assert = require('assert'); const crypto = require('crypto'); diff --git a/test/parallel/test-webcrypto-sign-verify.js b/test/parallel/test-webcrypto-sign-verify.js index 26e66d9aa0fa8b..0a6f5cffe7b934 100644 --- a/test/parallel/test-webcrypto-sign-verify.js +++ b/test/parallel/test-webcrypto-sign-verify.js @@ -173,7 +173,7 @@ if (!process.features.openssl_is_boringssl) { } // Test Sign/Verify ML-DSA -if (hasOpenSSL(3, 5)) { +if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { async function test(name, data) { const ec = new TextEncoder(); const { publicKey, privateKey } = await subtle.generateKey({ diff --git a/test/parallel/test-webcrypto-webidl.js b/test/parallel/test-webcrypto-webidl.js index 99386176c4d6ef..493d0093996b6d 100644 --- a/test/parallel/test-webcrypto-webidl.js +++ b/test/parallel/test-webcrypto-webidl.js @@ -16,6 +16,21 @@ const prefix = "Failed to execute 'fn' on 'interface'"; const context = '1st argument'; const opts = { prefix, context }; +function asIdlDictionary(value) { + return { __proto__: null, ...value }; +} + +function assertIdlDictionary(actual, expected) { + assert.deepStrictEqual(actual, asIdlDictionary(expected)); +} + +function assertJsonWebKey(actual, expected) { + const idlDictionary = asIdlDictionary(expected); + if (idlDictionary.oth !== undefined) + idlDictionary.oth = idlDictionary.oth.map(asIdlDictionary); + assert.deepStrictEqual(actual, idlDictionary); +} + // Required arguments.length { assert.throws(() => webidl.requiredArguments(0, 3, { prefix }), { @@ -158,6 +173,14 @@ const opts = { prefix, context }; code: 'ERR_INVALID_ARG_TYPE', message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.` }); + + { + const resizable = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(resizable); + + // TODO(panva): Reject resizable backing stores in a semver-major + assert.deepStrictEqual(converters.BigInteger(view), view); + } } // BufferSource @@ -194,6 +217,39 @@ const opts = { prefix, context }; code: 'ERR_INVALID_ARG_TYPE', message: `${prefix}: ${context} is a view on a SharedArrayBuffer, which is not allowed.` }); + + { + const resizable = new ArrayBuffer(8, { maxByteLength: 16 }); + const view = new Uint8Array(resizable); + const dataView = new DataView(resizable); + + // TODO(panva): Reject resizable backing stores in a semver-major by + // removing the crypto/webidl BufferSource override. + assert.deepStrictEqual(converters.BufferSource(resizable), resizable); + assert.deepStrictEqual(converters.BufferSource(view), view); + assert.deepStrictEqual(converters.BufferSource(dataView), dataView); + const resizableError = { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: `${prefix}: ${context} is backed by a resizable ` + + 'ArrayBuffer, which is not allowed.', + }; + assert.throws(() => converters.BufferSource(resizable, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + assert.throws(() => converters.BufferSource(view, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + assert.throws(() => converters.BufferSource(dataView, { + __proto__: null, + ...opts, + allowResizable: false, + }), resizableError); + } } // CryptoKey @@ -231,8 +287,8 @@ const opts = { prefix, context }; { oth: [] }, { oth: [{ r: '', d: '', t: '' }] }, ]) { - assert.deepStrictEqual(converters.JsonWebKey(good), good); - assert.deepStrictEqual(converters.JsonWebKey({ ...good, filtered: 'out' }), good); + assertJsonWebKey(converters.JsonWebKey(good), good); + assertJsonWebKey(converters.JsonWebKey({ ...good, filtered: 'out' }), good); } } @@ -255,7 +311,7 @@ const opts = { prefix, context }; assert.throws(() => converters.KeyFormat(bad, opts), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', - message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyFormat.`, + message: `${prefix}: ${context} '${bad}' is not a valid enum value of type KeyFormat.`, }); } } @@ -283,7 +339,7 @@ const opts = { prefix, context }; assert.throws(() => converters.KeyUsage(bad, opts), { name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE', - message: `${prefix}: ${context} value '${bad}' is not a valid enum value of type KeyUsage.`, + message: `${prefix}: ${context} '${bad}' is not a valid enum value of type KeyUsage.`, }); } } @@ -291,12 +347,12 @@ const opts = { prefix, context }; // Algorithm { const good = { name: 'RSA-PSS' }; - assert.deepStrictEqual(converters.Algorithm({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.Algorithm({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.Algorithm({}, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'Algorithm' because 'name' is required in 'Algorithm'.`, + message: `${prefix}: ${context} cannot be converted to 'Algorithm' because 'name' is required in 'Algorithm'.`, }); } @@ -316,12 +372,12 @@ const opts = { prefix, context }; publicExponent: new Uint8Array([1, 0, 1]), }, ]) { - assert.deepStrictEqual(converters.RsaHashedKeyGenParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaHashedKeyGenParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'publicExponent', 'modulusLength']) { assert.throws(() => converters.RsaHashedKeyGenParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaHashedKeyGenParams' because '${required}' is required in 'RsaHashedKeyGenParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaHashedKeyGenParams' because '${required}' is required in 'RsaHashedKeyGenParams'.`, }); } } @@ -333,11 +389,11 @@ const opts = { prefix, context }; { name: 'RSA-OAEP', hash: { name: 'SHA-1' } }, { name: 'RSA-OAEP', hash: 'SHA-1' }, ]) { - assert.deepStrictEqual(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaHashedImportParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.RsaHashedImportParams({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaHashedImportParams' because 'hash' is required in 'RsaHashedImportParams'.`, }); } } @@ -345,19 +401,19 @@ const opts = { prefix, context }; // RsaPssParams { const good = { name: 'RSA-PSS', saltLength: 20 }; - assert.deepStrictEqual(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaPssParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.RsaPssParams({ ...good, saltLength: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'RsaPssParams' because 'saltLength' is required in 'RsaPssParams'.`, + message: `${prefix}: ${context} cannot be converted to 'RsaPssParams' because 'saltLength' is required in 'RsaPssParams'.`, }); } // RsaOaepParams { for (const good of [{ name: 'RSA-OAEP' }, { name: 'RSA-OAEP', label: Buffer.alloc(0) }]) { - assert.deepStrictEqual(converters.RsaOaepParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.RsaOaepParams({ ...good, filtered: 'out' }, opts), good); } } @@ -367,12 +423,12 @@ const opts = { prefix, context }; const { [name]: converter } = converters; const good = { name: 'ECDSA', namedCurve: 'P-256' }; - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, namedCurve: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'namedCurve' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'namedCurve' is required in '${name}'.`, }); } } @@ -383,11 +439,11 @@ const opts = { prefix, context }; { name: 'ECDSA', hash: { name: 'SHA-1' } }, { name: 'ECDSA', hash: 'SHA-1' }, ]) { - assert.deepStrictEqual(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.EcdsaParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.EcdsaParams({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'EcdsaParams' because 'hash' is required in 'EcdsaParams'.`, + message: `${prefix}: ${context} cannot be converted to 'EcdsaParams' because 'hash' is required in 'EcdsaParams'.`, }); } } @@ -403,11 +459,11 @@ const opts = { prefix, context }; { name: 'HMAC', hash: 'SHA-1' }, { name: 'HMAC', hash: 'SHA-1', length: 32 }, ]) { - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, hash: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'hash' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'hash' is required in '${name}'.`, }); } } @@ -419,12 +475,12 @@ const opts = { prefix, context }; const { [name]: converter } = converters; const good = { name: 'AES-CBC', length: 128 }; - assert.deepStrictEqual(converter({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converter({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converter({ ...good, length: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to '${name}' because 'length' is required in '${name}'.`, + message: `${prefix}: ${context} cannot be converted to '${name}' because 'length' is required in '${name}'.`, }); } } @@ -435,12 +491,12 @@ const opts = { prefix, context }; { name: 'HKDF', hash: { name: 'SHA-1' }, salt: Buffer.alloc(0), info: Buffer.alloc(0) }, { name: 'HKDF', hash: 'SHA-1', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.HkdfParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.HkdfParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'salt', 'info']) { assert.throws(() => converters.HkdfParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'HkdfParams' because '${required}' is required in 'HkdfParams'.`, + message: `${prefix}: ${context} cannot be converted to 'HkdfParams' because '${required}' is required in 'HkdfParams'.`, }); } } @@ -452,26 +508,64 @@ const opts = { prefix, context }; { name: 'PBKDF2', hash: { name: 'SHA-1' }, iterations: 5, salt: Buffer.alloc(0) }, { name: 'PBKDF2', hash: 'SHA-1', iterations: 5, salt: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.Pbkdf2Params({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.Pbkdf2Params({ ...good, filtered: 'out' }, opts), good); for (const required of ['hash', 'iterations', 'salt']) { assert.throws(() => converters.Pbkdf2Params({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`, + message: `${prefix}: ${context} cannot be converted to 'Pbkdf2Params' because '${required}' is required in 'Pbkdf2Params'.`, }); } } } +// Argon2Params +{ + const good = { + name: 'Argon2id', + memory: 8, + nonce: Buffer.alloc(8), + parallelism: 1, + passes: 1, + }; + + assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); + + assertIdlDictionary( + converters.Argon2Params({ + ...good, + associatedData: Buffer.alloc(0), + secretValue: Buffer.alloc(0), + }, opts), + { + ...good, + associatedData: Buffer.alloc(0), + secretValue: Buffer.alloc(0), + }); + + for (const required of ['memory', 'nonce', 'parallelism', 'passes']) { + assert.throws(() => converters.Argon2Params({ ...good, [required]: undefined }, opts), { + name: 'TypeError', + code: 'ERR_MISSING_OPTION', + message: `${prefix}: ${context} cannot be converted to 'Argon2Params' because '${required}' is required in 'Argon2Params'.`, + }); + } + + assert.throws(() => converters.Argon2Params({ ...good, passes: 0 }, opts), { + name: 'OperationError', + message: 'passes must be > 0', + }); +} + // AesCbcParams { const good = { name: 'AES-CBC', iv: Buffer.alloc(16) }; - assert.deepStrictEqual(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AesCbcParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.AesCbcParams({ ...good, iv: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AesCbcParams' because 'iv' is required in 'AesCbcParams'.`, }); } @@ -482,12 +576,12 @@ const opts = { prefix, context }; { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64 }, { name: 'AES-GCM', iv: Buffer.alloc(0), tagLength: 64, additionalData: Buffer.alloc(0) }, ]) { - assert.deepStrictEqual(converters.AeadParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AeadParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.AeadParams({ ...good, iv: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AeadParams' because 'iv' is required in 'AeadParams'.`, }); } } @@ -495,13 +589,13 @@ const opts = { prefix, context }; // AesCtrParams { const good = { name: 'AES-CTR', counter: Buffer.alloc(16), length: 20 }; - assert.deepStrictEqual(converters.AesCtrParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.AesCtrParams({ ...good, filtered: 'out' }, opts), good); for (const required of ['counter', 'length']) { assert.throws(() => converters.AesCtrParams({ ...good, [required]: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'AesCtrParams' because '${required}' is required in 'AesCtrParams'.`, + message: `${prefix}: ${context} cannot be converted to 'AesCtrParams' because '${required}' is required in 'AesCtrParams'.`, }); } } @@ -510,12 +604,12 @@ const opts = { prefix, context }; { subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']).then((kp) => { const good = { name: 'ECDH', public: kp.publicKey }; - assert.deepStrictEqual(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.EcdhKeyDeriveParams({ ...good, filtered: 'out' }, opts), good); assert.throws(() => converters.EcdhKeyDeriveParams({ ...good, public: undefined }, opts), { name: 'TypeError', code: 'ERR_MISSING_OPTION', - message: `${prefix}: ${context} can not be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`, + message: `${prefix}: ${context} cannot be converted to 'EcdhKeyDeriveParams' because 'public' is required in 'EcdhKeyDeriveParams'.`, }); }).then(common.mustCall()); } @@ -526,6 +620,28 @@ const opts = { prefix, context }; { name: 'Ed448', context: new Uint8Array() }, { name: 'Ed448' }, ]) { - assert.deepStrictEqual(converters.ContextParams({ ...good, filtered: 'out' }, opts), good); + assertIdlDictionary(converters.ContextParams({ ...good, filtered: 'out' }, opts), good); } } + +// Argon2Params +{ + const maxParallelism = 2 ** 24 - 1; + const good = { + name: 'Argon2id', + nonce: Buffer.alloc(8), + parallelism: maxParallelism, + memory: 8 * maxParallelism, + passes: 1, + }; + assertIdlDictionary(converters.Argon2Params({ ...good, filtered: 'out' }, opts), good); + + assert.throws(() => converters.Argon2Params({ + ...good, + parallelism: maxParallelism + 1, + memory: 8 * (maxParallelism + 1), + }, opts), { + name: 'OperationError', + message: 'parallelism must be > 0 and <= 16777215', + }); +} diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index a0cbe2c324f786..49f63e215fadfc 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -39,25 +39,20 @@ const kWrappingData = { }, pair: false }, -}; - -if (!process.features.openssl_is_boringssl) { - kWrappingData['AES-KW'] = { + 'AES-KW': { generate: { length: 128 }, wrap: { }, pair: false - }; - kWrappingData['ChaCha20-Poly1305'] = { + }, + 'ChaCha20-Poly1305': { wrap: { iv: new Uint8Array(12), additionalData: new Uint8Array(16), tagLength: 128 }, pair: false - }; -} else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); -} + } +}; if (hasOpenSSL(3)) { kWrappingData['AES-OCB'] = { @@ -179,13 +174,6 @@ async function generateKeysToWrap() { usages: ['encrypt', 'decrypt'], pair: false, }, - { - algorithm: { - name: 'ChaCha20-Poly1305' - }, - usages: ['encrypt', 'decrypt'], - pair: false, - }, { algorithm: { name: 'HMAC', @@ -195,22 +183,24 @@ async function generateKeysToWrap() { usages: ['sign', 'verify'], pair: false, }, - ]; - - if (!process.features.openssl_is_boringssl) { - parameters.push({ + { algorithm: { name: 'AES-KW', length: 128 }, usages: ['wrapKey', 'unwrapKey'], pair: false, - }); - } else { - common.printSkipMessage('Skipping unsupported AES-KW test case'); - } + }, + { + algorithm: { + name: 'ChaCha20-Poly1305' + }, + usages: ['encrypt', 'decrypt'], + pair: false, + }, + ]; - if (hasOpenSSL(3, 5)) { + if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { parameters.push({ algorithm: { name }, diff --git a/test/parallel/test-x509-escaping.js b/test/parallel/test-x509-escaping.js index a5937a09cb1535..ab91e334555669 100644 --- a/test/parallel/test-x509-escaping.js +++ b/test/parallel/test-x509-escaping.js @@ -438,7 +438,9 @@ const { hasOpenSSL3 } = require('../common/crypto'); const cert = fixtures.readKey('incorrect_san_correct_subject-cert.pem'); // The hostname is the CN, but not a SAN entry. - const servername = process.features.openssl_is_boringssl ? undefined : 'good.example.com'; + const servername = 'good.example.com'; + const cnFallback = process.features.openssl_is_boringssl ? undefined : + servername; const certX509 = new X509Certificate(cert); assert.strictEqual(certX509.subject, `CN=${servername}`); assert.strictEqual(certX509.subjectAltName, 'DNS:evil.example.com'); @@ -448,7 +450,7 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.strictEqual(certX509.checkHost(servername, { subject: 'default' }), undefined); assert.strictEqual(certX509.checkHost(servername, { subject: 'always' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'never' }), undefined); @@ -483,11 +485,13 @@ const { hasOpenSSL3 } = require('../common/crypto'); assert.strictEqual(certX509.subjectAltName, 'IP Address:1.2.3.4'); // The newer X509Certificate API allows customizing this behavior: - assert.strictEqual(certX509.checkHost(servername), servername); + const cnFallback = process.features.openssl_is_boringssl ? undefined : + servername; + assert.strictEqual(certX509.checkHost(servername), cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'default' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'always' }), - servername); + cnFallback); assert.strictEqual(certX509.checkHost(servername, { subject: 'never' }), undefined); diff --git a/test/pummel/test-crypto-dh-keys.js b/test/pummel/test-crypto-dh-keys.js index ba67bc5717af62..8aa1e30e354f95 100644 --- a/test/pummel/test-crypto-dh-keys.js +++ b/test/pummel/test-crypto-dh-keys.js @@ -33,11 +33,14 @@ if (common.isPi()) { const assert = require('assert'); const crypto = require('crypto'); -[ 'modp1', 'modp2', 'modp5', 'modp14', 'modp15', 'modp16', 'modp17' ] -.forEach((name) => { - // modp1 is 768 bits, FIPS requires >= 1024 - if (name === 'modp1' && crypto.getFips()) { - return; +for (const name of ['modp1', 'modp2', 'modp5', 'modp14', 'modp15', 'modp16', 'modp17']) { + // modp1 is 768 bits, FIPS requires >= 1024. + // BoringSSL does not support modp1 or modp2. + if ((name === 'modp1' && crypto.getFips()) || + (process.features.openssl_is_boringssl && + (name === 'modp1' || name === 'modp2'))) { + common.printSkipMessage(`Skipping unsupported ${name} test case`); + continue; } const group1 = crypto.getDiffieHellman(name); const group2 = crypto.getDiffieHellman(name); @@ -46,4 +49,4 @@ const crypto = require('crypto'); const key1 = group1.computeSecret(group2.getPublicKey()); const key2 = group2.computeSecret(group1.getPublicKey()); assert.deepStrictEqual(key1, key2); -}); +} diff --git a/test/pummel/test-webcrypto-derivebits-pbkdf2.js b/test/pummel/test-webcrypto-derivebits-pbkdf2.js index 7cbcafd020edf7..bfb01ac0c94fe0 100644 --- a/test/pummel/test-webcrypto-derivebits-pbkdf2.js +++ b/test/pummel/test-webcrypto-derivebits-pbkdf2.js @@ -24,12 +24,12 @@ const kDerivedKeyTypes = [ ['AES-CTR', 256, undefined, 'encrypt', 'decrypt'], ['AES-GCM', 128, undefined, 'encrypt', 'decrypt'], ['AES-GCM', 256, undefined, 'encrypt', 'decrypt'], - ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], - ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ['HMAC', 256, 'SHA-1', 'sign', 'verify'], ['HMAC', 256, 'SHA-256', 'sign', 'verify'], ['HMAC', 256, 'SHA-384', 'sign', 'verify'], ['HMAC', 256, 'SHA-512', 'sign', 'verify'], + ['AES-KW', 128, undefined, 'wrapKey', 'unwrapKey'], + ['AES-KW', 256, undefined, 'wrapKey', 'unwrapKey'], ]; const kPasswords = { diff --git a/test/pummel/test-webcrypto-kangarootwelve-32bit-overflow.js b/test/pummel/test-webcrypto-kangarootwelve-32bit-overflow.js new file mode 100644 index 00000000000000..cb399ec18b810e --- /dev/null +++ b/test/pummel/test-webcrypto-kangarootwelve-32bit-overflow.js @@ -0,0 +1,41 @@ +'use strict'; +const common = require('../common'); + +if (!common.hasCrypto) + common.skip('missing crypto'); + +// KangarooTwelve: data + customization size_t overflow on 32-bit platforms. +// On 32-bit, msg_len + custom_len + LengthEncode(custom_len).size() can wrap +// size_t, causing an undersized allocation and heap buffer overflow. +// This test verifies the guard rejects such inputs. +// When kMaxLength < 2^32 two max-sized buffers can overflow a 32-bit size_t. + +const { kMaxLength } = require('buffer'); + +if (kMaxLength >= 2 ** 32) + common.skip('only relevant when kMaxLength < 2^32'); + +const assert = require('assert'); +const { subtle } = globalThis.crypto; + +let data, customization; +try { + data = new Uint8Array(kMaxLength); + customization = new Uint8Array(kMaxLength); +} catch { + common.skip('insufficient memory to allocate test buffers'); +} + +(async () => { + await assert.rejects( + subtle.digest( + { name: 'KT128', outputLength: 256, customization }, + data), + { name: 'OperationError' }); + + await assert.rejects( + subtle.digest( + { name: 'KT256', outputLength: 512, customization }, + data), + { name: 'OperationError' }); +})().then(common.mustCall()); diff --git a/test/sequential/test-tls-connect.js b/test/sequential/test-tls-connect.js index 189b9afa6352bb..ca8a1d8128554e 100644 --- a/test/sequential/test-tls-connect.js +++ b/test/sequential/test-tls-connect.js @@ -57,5 +57,5 @@ const tls = require('tls'); port: common.PORT, ciphers: 'rick-128-roll', }, common.mustNotCall()); - }, /no cipher match/i); + }, /no[_ ]cipher[_ ]match/i); } diff --git a/test/sequential/test-tls-psk-client.js b/test/sequential/test-tls-psk-client.js index 65e628a6f4e0eb..2eb6228f79f265 100644 --- a/test/sequential/test-tls-psk-client.js +++ b/test/sequential/test-tls-psk-client.js @@ -5,6 +5,11 @@ if (!common.hasCrypto) { common.skip('missing crypto'); } +if (process.features.openssl_is_boringssl) { + require('../common/boringssl').testPskTls13Unsupported(); + return; +} + const { opensslCli } = require('../common/crypto'); if (!opensslCli) { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index d7f2d4aca70f5d..5d1d630c436311 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', @@ -25,6 +36,8 @@ if (!hasOpenSSL(3, 0)) { 'generateKey/successes_kmac.tentative.https.any.js', 'import_export/AES-OCB_importKey.tentative.https.any.js', 'import_export/KMAC_importKey.tentative.https.any.js', + 'serialization/aes-ocb.tentative.https.any.js', + 'serialization/kmac.tentative.https.any.js', 'sign_verify/kmac.tentative.https.any.js'); } @@ -34,7 +47,7 @@ if (!hasOpenSSL(3, 2)) { 'import_export/Argon2_importKey.tentative.https.any.js'); } -if (!hasOpenSSL(3, 5)) { +if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { skip( 'encap_decap/encap_decap_bits.tentative.https.any.js', 'encap_decap/encap_decap_keys.tentative.https.any.js', @@ -44,9 +57,53 @@ if (!hasOpenSSL(3, 5)) { 'generateKey/successes_ML-KEM.tentative.https.any.js', 'import_export/ML-DSA_importKey.tentative.https.any.js', 'import_export/ML-KEM_importKey.tentative.https.any.js', + 'serialization/mldsa.tentative.https.any.js', + 'serialization/mlkem.tentative.https.any.js', 'sign_verify/mldsa.tentative.https.any.js'); + + skipSubtests( + ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); } +if (process.features.openssl_is_boringssl) { + skip( + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js', + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js', + 'digest/cshake.tentative.https.any.js', + 'digest/sha3.tentative.https.any.js', + 'generateKey/failures_Ed448.tentative.https.any.js', + 'generateKey/failures_X448.tentative.https.any.js', + 'generateKey/successes_Ed448.tentative.https.any.js', + 'generateKey/successes_X448.tentative.https.any.js', + 'import_export/okp_importKey_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_Ed448.tentative.https.any.js', + 'import_export/okp_importKey_failures_X448.tentative.https.any.js', + 'import_export/okp_importKey_X448.tentative.https.any.js', + 'serialization/ed448.tentative.https.any.js', + 'serialization/x448.tentative.https.any.js', + 'sign_verify/eddsa_curve448.tentative.https.any.js'); + + skipSubtests( + ['encap_decap/encap_decap_bits.tentative.https.any.js', /ml-kem-512/i], + ['encap_decap/encap_decap_keys.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/failures_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['generateKey/successes_ML-KEM.tentative.https.any.js', /ml-kem-512/i], + ['import_export/ML-KEM_importKey.tentative.https.any.js', /ml-kem-512/i], + ['serialization/mlkem.tentative.https.any.js', /ml-kem-512/i], + ['supports-modern.tentative.https.any.js', /ml-kem-512/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); + const cshakeExpectedFailures = ['cSHAKE128', 'cSHAKE256'].flatMap((algorithm) => { return [0, 256, 384, 512].flatMap((length) => { return ['empty', 'short', 'medium'].flatMap((size) => { @@ -95,7 +152,8 @@ const kmacExpectedFailures = kmacVectorNames.flatMap((name) => { }); module.exports = { - ...conditionalSkips, + ...conditionalFileSkips, + ...conditionalSubtestSkips, 'algorithm-discards-context.https.window.js': { 'skip': 'Not relevant in Node.js context', }, @@ -133,13 +191,13 @@ module.exports = { ], }, }, - 'digest/cshake.tentative.https.any.js': { + 'digest/cshake.tentative.https.any.js': conditionalFileSkips['digest/cshake.tentative.https.any.js'] ?? { 'fail': { 'note': 'WPT still uses CShakeParams.length; implementation moved to CShakeParams.outputLength', 'expected': cshakeExpectedFailures, }, }, - 'sign_verify/kmac.tentative.https.any.js': conditionalSkips['sign_verify/kmac.tentative.https.any.js'] ?? { + 'sign_verify/kmac.tentative.https.any.js': conditionalFileSkips['sign_verify/kmac.tentative.https.any.js'] ?? { 'fail': { 'note': 'WPT still uses KmacParams.length; implementation moved to KmacParams.outputLength', 'expected': kmacExpectedFailures, diff --git a/test/wpt/status/resource-timing.json b/test/wpt/status/resource-timing.json index 6406b88d3266f5..74de815387ed33 100644 --- a/test/wpt/status/resource-timing.json +++ b/test/wpt/status/resource-timing.json @@ -31,7 +31,17 @@ "PerformanceResourceTiming interface: resource must inherit property \"firstInterimResponseStart\" with the proper type", "PerformanceResourceTiming interface: resource must inherit property \"renderBlockingStatus\" with the proper type", "PerformanceResourceTiming interface: resource must inherit property \"contentType\" with the proper type", - "PerformanceResourceTiming interface: default toJSON operation on resource" + "PerformanceResourceTiming interface: default toJSON operation on resource", + "PerformanceResourceTiming interface: attribute workerRouterEvaluationStart", + "PerformanceResourceTiming interface: attribute workerCacheLookupStart", + "PerformanceResourceTiming interface: attribute workerMatchedRouterSource", + "PerformanceResourceTiming interface: attribute workerFinalRouterSource", + "PerformanceResourceTiming interface: attribute contentEncoding", + "PerformanceResourceTiming interface: resource must inherit property \"workerRouterEvaluationStart\" with the proper type", + "PerformanceResourceTiming interface: resource must inherit property \"workerCacheLookupStart\" with the proper type", + "PerformanceResourceTiming interface: resource must inherit property \"workerMatchedRouterSource\" with the proper type", + "PerformanceResourceTiming interface: resource must inherit property \"workerFinalRouterSource\" with the proper type", + "PerformanceResourceTiming interface: resource must inherit property \"contentEncoding\" with the proper type" ] } } diff --git a/test/wpt/test-webcrypto.js b/test/wpt/test-webcrypto.js index 0d53a51901bbb9..ec0e1359df3745 100644 --- a/test/wpt/test-webcrypto.js +++ b/test/wpt/test-webcrypto.js @@ -8,6 +8,19 @@ const { WPTRunner } = require('../common/wpt'); const runner = new WPTRunner('WebCryptoAPI'); +runner.setInitScript(` +if (Uint8Array.fromHex === undefined) { + Object.defineProperty(Uint8Array, 'fromHex', { + __proto__: null, + configurable: true, + writable: true, + value(hex) { + return new Uint8Array(Buffer.from(hex, 'hex')); + }, + }); +} +`); + runner.pretendGlobalThisAs('Window'); runner.runJsTests(); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 3f79c7e441b767..00aef9c15e5ac8 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -124,10 +124,12 @@ const customTypesMap = { 'RsaPssParams': 'webcrypto.html#class-rsapssparams', 'ContextParams': 'webcrypto.html#class-contextparams', 'CShakeParams': 'webcrypto.html#class-cshakeparams', + 'KangarooTwelveParams': 'webcrypto.html#class-kangarootwelveparams', 'KmacImportParams': 'webcrypto.html#class-kmacimportparams', 'KmacKeyAlgorithm': 'webcrypto.html#class-kmackeyalgorithm', 'KmacKeyGenParams': 'webcrypto.html#class-kmackeygenparams', 'KmacParams': 'webcrypto.html#class-kmacparams', + 'TurboShakeParams': 'webcrypto.html#class-turboshakeparams', 'dgram.Socket': 'dgram.html#class-dgramsocket', diff --git a/tools/eslint-rules/no-cryptokey-public-accessors.js b/tools/eslint-rules/no-cryptokey-public-accessors.js new file mode 100644 index 00000000000000..cec9198d740fc3 --- /dev/null +++ b/tools/eslint-rules/no-cryptokey-public-accessors.js @@ -0,0 +1,277 @@ +/** + * @file Prevent internal code from using public CryptoKey accessors. + */ +'use strict'; + +const { isRequireCall, isString } = require('./rules-utils.js'); + +const CRYPTO_KEYS_MODULE = 'internal/crypto/keys'; +const WEBCRYPTO_MODULE = 'internal/crypto/webcrypto'; + +const accessors = new Map([ + ['type', 'getCryptoKeyType(key)'], + ['extractable', 'getCryptoKeyExtractable(key)'], + ['algorithm', 'getCryptoKeyAlgorithm(key)'], + ['usages', 'getCryptoKeyUsages(key)'], +]); + +const cryptoKeyClassNames = new Set([ + 'CryptoKey', + 'InternalCryptoKey', +]); + +function isCryptoKeyModuleRequire(node) { + return node?.type === 'CallExpression' && + isRequireCall(node) && + isString(node.arguments[0]) && + (node.arguments[0].value === CRYPTO_KEYS_MODULE || + node.arguments[0].value === WEBCRYPTO_MODULE); +} + +function getPropertyName(node) { + if (!node) return undefined; + if (node.computed) { + return node.property.type === 'Literal' ? node.property.value : undefined; + } + return node.property.name; +} + +function getIdentifierArgument(node) { + const arg = node.arguments[0]; + return arg?.type === 'Identifier' ? arg.name : undefined; +} + +function isNodeWithin(node, ancestor) { + return node.range[0] >= ancestor.range[0] && + node.range[1] <= ancestor.range[1]; +} + +function exits(statement) { + if (!statement) return false; + switch (statement.type) { + case 'BlockStatement': + return statement.body.length > 0 && exits(statement.body.at(-1)); + case 'ReturnStatement': + case 'ThrowStatement': + return true; + default: + return false; + } +} + +function findStatementInBlock(node) { + let current = node; + while (current?.parent) { + if ((current.parent.type === 'BlockStatement' || + current.parent.type === 'Program') && + current.parent.body.includes(current)) { + return { block: current.parent, statement: current }; + } + current = current.parent; + } +} + +function isWebIDLCryptoKeyConverter(node) { + if (node?.type !== 'CallExpression') return false; + if (node.callee.type !== 'MemberExpression') return false; + if (getPropertyName(node.callee) !== 'CryptoKey') return false; + + const converter = node.callee.object; + return converter?.type === 'MemberExpression' && + getPropertyName(converter) === 'converters'; +} + +module.exports = { + meta: { + messages: { + noPublicAccessor: 'Use `{{replacement}}` instead of the public CryptoKey `{{property}}` accessor.', + }, + schema: [], + }, + + create(context) { + const isCryptoKeyNames = new Set(); + const namespaceNames = new Set(); + const knownCryptoKeyNames = new Set(); + const knownCryptoKeyClassNames = new Set(cryptoKeyClassNames); + + function isIsCryptoKeyCall(node) { + if (node?.type !== 'CallExpression') return false; + + if (node.callee.type === 'Identifier') { + return isCryptoKeyNames.has(node.callee.name); + } + + if (node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.object.type === 'Identifier' && + namespaceNames.has(node.callee.object.name)) { + return node.callee.property.name === 'isCryptoKey'; + } + + return false; + } + + function getConsequentCryptoKeys(test) { + const names = new Set(); + if (isIsCryptoKeyCall(test)) { + names.add(getIdentifierArgument(test)); + } else if (test?.type === 'LogicalExpression' && test.operator === '&&') { + for (const name of getConsequentCryptoKeys(test.left)) { + names.add(name); + } + for (const name of getConsequentCryptoKeys(test.right)) { + names.add(name); + } + } + names.delete(undefined); + return names; + } + + function getAlternateCryptoKeys(test) { + const names = new Set(); + if (test?.type === 'UnaryExpression' && + test.operator === '!' && + isIsCryptoKeyCall(test.argument)) { + names.add(getIdentifierArgument(test.argument)); + } + names.delete(undefined); + return names; + } + + function isCryptoKeyFactory(node) { + if (node?.type === 'NewExpression' && + node.callee.type === 'Identifier') { + return knownCryptoKeyClassNames.has(node.callee.name); + } + + return isWebIDLCryptoKeyConverter(node); + } + + function isInCryptoKeyBranch(name, node) { + for (let current = node.parent; current; current = current.parent) { + if (current.type !== 'IfStatement') continue; + if (isNodeWithin(node, current.consequent) && + getConsequentCryptoKeys(current.test).has(name)) { + return true; + } + if (current.alternate && + isNodeWithin(node, current.alternate) && + getAlternateCryptoKeys(current.test).has(name)) { + return true; + } + } + return false; + } + + function followsExitingCryptoKeyGuard(name, node) { + const location = findStatementInBlock(node); + if (!location) return false; + const index = location.block.body.indexOf(location.statement); + for (let i = 0; i < index; i++) { + const statement = location.block.body[i]; + if (statement.type === 'IfStatement' && + exits(statement.consequent) && + getAlternateCryptoKeys(statement.test).has(name)) { + return true; + } + } + return false; + } + + function followsLogicalCryptoKeyCheck(name, node) { + for (let current = node; current.parent; current = current.parent) { + const parent = current.parent; + if (parent.type !== 'LogicalExpression' || parent.operator !== '&&') { + continue; + } + if (parent.right === current && + getConsequentCryptoKeys(parent.left).has(name)) { + return true; + } + } + return false; + } + + function isInsideCryptoKeyClass(node) { + for (let current = node.parent; current; current = current.parent) { + if (current.type !== 'ClassDeclaration' && + current.type !== 'ClassExpression') { + continue; + } + + const className = current.id?.name; + const superName = current.superClass?.type === 'Identifier' ? + current.superClass.name : undefined; + return knownCryptoKeyClassNames.has(className) || + knownCryptoKeyClassNames.has(superName); + } + return false; + } + + function isKnownCryptoKey(node) { + if (node.type === 'ThisExpression') { + return isInsideCryptoKeyClass(node); + } + + if (node.type !== 'Identifier') return false; + return knownCryptoKeyNames.has(node.name) || + isInCryptoKeyBranch(node.name, node) || + followsLogicalCryptoKeyCheck(node.name, node) || + followsExitingCryptoKeyGuard(node.name, node); + } + + return { + VariableDeclarator(node) { + if (isCryptoKeyModuleRequire(node.init)) { + if (node.id.type === 'Identifier') { + namespaceNames.add(node.id.name); + return; + } + + if (node.id.type !== 'ObjectPattern') return; + + for (const property of node.id.properties) { + if (property.type !== 'Property') continue; + const keyName = property.key.name ?? property.key.value; + if (property.value.type !== 'Identifier') continue; + const localName = property.value.name; + if (keyName === 'isCryptoKey') { + isCryptoKeyNames.add(localName); + } else if (cryptoKeyClassNames.has(keyName)) { + knownCryptoKeyClassNames.add(localName); + } + } + return; + } + + if (node.id.type === 'Identifier' && isCryptoKeyFactory(node.init)) { + knownCryptoKeyNames.add(node.id.name); + } + }, + + AssignmentExpression(node) { + if (node.left.type === 'Identifier' && isCryptoKeyFactory(node.right)) { + knownCryptoKeyNames.add(node.left.name); + } + }, + + MemberExpression(node) { + const property = getPropertyName(node); + const replacement = accessors.get(property); + if (replacement === undefined) return; + if (!isKnownCryptoKey(node.object)) return; + + context.report({ + node: node.property, + messageId: 'noPublicAccessor', + data: { + property, + replacement, + }, + }); + }, + + }; + }, +}; diff --git a/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js b/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js new file mode 100644 index 00000000000000..19e13437247a5e --- /dev/null +++ b/tools/eslint-rules/no-keyobject-cryptokey-instanceof.js @@ -0,0 +1,122 @@ +/** + * @file Prevent internal code from brand-checking keys with instanceof. + */ +'use strict'; + +const { isRequireCall, isString } = require('./rules-utils.js'); + +const CRYPTO_KEYS_MODULE = 'internal/crypto/keys'; +const WEBCRYPTO_MODULE = 'internal/crypto/webcrypto'; + +const keyObjectClassNames = new Set([ + 'KeyObject', + 'SecretKeyObject', + 'AsymmetricKeyObject', + 'PublicKeyObject', + 'PrivateKeyObject', +]); + +const cryptoKeyClassNames = new Set([ + 'CryptoKey', + 'InternalCryptoKey', +]); + +function isKeyModuleRequire(node) { + return node?.type === 'CallExpression' && + isRequireCall(node) && + isString(node.arguments[0]) && + (node.arguments[0].value === CRYPTO_KEYS_MODULE || + node.arguments[0].value === WEBCRYPTO_MODULE); +} + +function getPropertyName(node) { + if (!node) return undefined; + if (node.computed) { + return node.property.type === 'Literal' ? node.property.value : undefined; + } + return node.property.name; +} + +module.exports = { + meta: { + messages: { + noKeyObjectInstanceof: 'Use `isKeyObject(value)` instead of `value instanceof KeyObject`.', + noCryptoKeyInstanceof: 'Use `isCryptoKey(value)` instead of `value instanceof CryptoKey`.', + }, + schema: [], + }, + + create(context) { + const namespaceNames = new Set(); + const keyObjectConstructorNames = new Set(); + const cryptoKeyConstructorNames = new Set(['CryptoKey']); + + function registerRequire(node) { + if (!isKeyModuleRequire(node.init)) return; + + if (node.id.type === 'Identifier') { + namespaceNames.add(node.id.name); + return; + } + + if (node.id.type !== 'ObjectPattern') return; + + for (const property of node.id.properties) { + if (property.type !== 'Property') continue; + const keyName = property.key.name ?? property.key.value; + if (property.value.type !== 'Identifier') continue; + const localName = property.value.name; + if (keyObjectClassNames.has(keyName)) { + keyObjectConstructorNames.add(localName); + } else if (cryptoKeyClassNames.has(keyName)) { + cryptoKeyConstructorNames.add(localName); + } + } + } + + function constructorKind(node) { + if (node.type === 'Identifier') { + if (keyObjectConstructorNames.has(node.name)) return 'KeyObject'; + if (cryptoKeyConstructorNames.has(node.name)) return 'CryptoKey'; + return undefined; + } + + if (node.type !== 'MemberExpression') return undefined; + + const property = getPropertyName(node); + if (node.object.type === 'Identifier') { + if (namespaceNames.has(node.object.name)) { + if (keyObjectClassNames.has(property)) return 'KeyObject'; + if (cryptoKeyClassNames.has(property)) return 'CryptoKey'; + } + if (node.object.name === 'globalThis' && + cryptoKeyClassNames.has(property)) { + return 'CryptoKey'; + } + } + + return undefined; + } + + return { + VariableDeclarator: registerRequire, + + BinaryExpression(node) { + if (node.operator !== 'instanceof') return; + + const kind = constructorKind(node.right); + if (kind === 'KeyObject') { + context.report({ + node, + messageId: 'noKeyObjectInstanceof', + }); + } else if (kind === 'CryptoKey') { + context.report({ + node, + messageId: 'noCryptoKeyInstanceof', + }); + } + }, + }; + }, +}; diff --git a/tools/eslint-rules/no-keyobject-public-accessors.js b/tools/eslint-rules/no-keyobject-public-accessors.js new file mode 100644 index 00000000000000..64b93f4a70e65e --- /dev/null +++ b/tools/eslint-rules/no-keyobject-public-accessors.js @@ -0,0 +1,284 @@ +/** + * @file Prevent internal code from using public KeyObject accessors. + */ +'use strict'; + +const { isRequireCall, isString } = require('./rules-utils.js'); + +const KEYOBJECT_MODULE = 'internal/crypto/keys'; + +const accessors = new Map([ + ['type', 'getKeyObjectType(key)'], + ['symmetricKeySize', 'getKeyObjectSymmetricKeySize(key)'], + ['asymmetricKeyType', 'getKeyObjectAsymmetricKeyType(key)'], + ['asymmetricKeyDetails', 'getKeyObjectAsymmetricKeyDetails(key)'], + ['equals', 'getKeyObjectType(key) and getKeyObjectHandle(key)'], +]); + +const keyObjectClassNames = new Set([ + 'KeyObject', + 'SecretKeyObject', + 'AsymmetricKeyObject', + 'PublicKeyObject', + 'PrivateKeyObject', +]); + +const keyObjectFactoryNames = new Set([ + 'createSecretKey', + 'createPublicKey', + 'createPrivateKey', +]); + +function isInternalCryptoKeysRequire(node) { + return node?.type === 'CallExpression' && + isRequireCall(node) && + isString(node.arguments[0]) && + node.arguments[0].value === KEYOBJECT_MODULE; +} + +function getPropertyName(node) { + if (!node) return undefined; + if (node.computed) { + return node.property.type === 'Literal' ? node.property.value : undefined; + } + return node.property.name; +} + +function getIdentifierArgument(node) { + const arg = node.arguments[0]; + return arg?.type === 'Identifier' ? arg.name : undefined; +} + +function isNodeWithin(node, ancestor) { + return node.range[0] >= ancestor.range[0] && + node.range[1] <= ancestor.range[1]; +} + +function exits(statement) { + if (!statement) return false; + switch (statement.type) { + case 'BlockStatement': + return statement.body.length > 0 && exits(statement.body.at(-1)); + case 'ReturnStatement': + case 'ThrowStatement': + return true; + default: + return false; + } +} + +function findStatementInBlock(node) { + let current = node; + while (current?.parent) { + if ((current.parent.type === 'BlockStatement' || + current.parent.type === 'Program') && + current.parent.body.includes(current)) { + return { block: current.parent, statement: current }; + } + current = current.parent; + } +} + +module.exports = { + meta: { + messages: { + noPublicAccessor: 'Use `{{replacement}}` instead of the public KeyObject `{{property}}` accessor.', + }, + schema: [], + }, + + create(context) { + const isKeyObjectNames = new Set(); + const namespaceNames = new Set(); + const knownKeyObjectNames = new Set(); + const knownKeyObjectClassNames = new Set(keyObjectClassNames); + + function isIsKeyObjectCall(node) { + if (node?.type !== 'CallExpression') return false; + + if (node.callee.type === 'Identifier') { + return isKeyObjectNames.has(node.callee.name); + } + + if (node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.object.type === 'Identifier' && + namespaceNames.has(node.callee.object.name)) { + return node.callee.property.name === 'isKeyObject'; + } + + return false; + } + + function getConsequentKeyObjects(test) { + const names = new Set(); + if (isIsKeyObjectCall(test)) { + names.add(getIdentifierArgument(test)); + } else if (test?.type === 'LogicalExpression' && test.operator === '&&') { + for (const name of getConsequentKeyObjects(test.left)) { + names.add(name); + } + for (const name of getConsequentKeyObjects(test.right)) { + names.add(name); + } + } + names.delete(undefined); + return names; + } + + function getAlternateKeyObjects(test) { + const names = new Set(); + if (test?.type === 'UnaryExpression' && + test.operator === '!' && + isIsKeyObjectCall(test.argument)) { + names.add(getIdentifierArgument(test.argument)); + } + names.delete(undefined); + return names; + } + + function isKeyObjectFactory(node) { + if (node?.type === 'NewExpression' && + node.callee.type === 'Identifier') { + return knownKeyObjectClassNames.has(node.callee.name); + } + + if (node?.type !== 'CallExpression') return false; + + if (node.callee.type === 'Identifier') { + return keyObjectFactoryNames.has(node.callee.name); + } + + if (node.callee.type !== 'MemberExpression') return false; + const object = node.callee.object; + const property = getPropertyName(node.callee); + if (object.type === 'Identifier' && + knownKeyObjectClassNames.has(object.name)) { + return property === 'from'; + } + return object.type === 'Identifier' && + namespaceNames.has(object.name) && + keyObjectFactoryNames.has(property); + } + + function isInKeyObjectBranch(name, node) { + for (let current = node.parent; current; current = current.parent) { + if (current.type !== 'IfStatement') continue; + if (isNodeWithin(node, current.consequent) && + getConsequentKeyObjects(current.test).has(name)) { + return true; + } + if (current.alternate && + isNodeWithin(node, current.alternate) && + getAlternateKeyObjects(current.test).has(name)) { + return true; + } + } + return false; + } + + function followsExitingKeyObjectGuard(name, node) { + const location = findStatementInBlock(node); + if (!location) return false; + const index = location.block.body.indexOf(location.statement); + for (let i = 0; i < index; i++) { + const statement = location.block.body[i]; + if (statement.type === 'IfStatement' && + exits(statement.consequent) && + getAlternateKeyObjects(statement.test).has(name)) { + return true; + } + } + return false; + } + + function followsLogicalKeyObjectCheck(name, node) { + for (let current = node; current.parent; current = current.parent) { + const parent = current.parent; + if (parent.type !== 'LogicalExpression' || parent.operator !== '&&') { + continue; + } + if (parent.right === current && + getConsequentKeyObjects(parent.left).has(name)) { + return true; + } + } + return false; + } + + function isInsideKeyObjectClass(node) { + for (let current = node.parent; current; current = current.parent) { + if (current.type !== 'ClassDeclaration' && + current.type !== 'ClassExpression') { + continue; + } + + const className = current.id?.name; + const superName = current.superClass?.type === 'Identifier' ? + current.superClass.name : undefined; + return knownKeyObjectClassNames.has(className) || + knownKeyObjectClassNames.has(superName); + } + return false; + } + + function isKnownKeyObject(node) { + if (node.type === 'ThisExpression') { + return isInsideKeyObjectClass(node); + } + + if (node.type !== 'Identifier') return false; + return knownKeyObjectNames.has(node.name) || + isInKeyObjectBranch(node.name, node) || + followsLogicalKeyObjectCheck(node.name, node) || + followsExitingKeyObjectGuard(node.name, node); + } + + return { + VariableDeclarator(node) { + if (isInternalCryptoKeysRequire(node.init)) { + if (node.id.type === 'Identifier') { + namespaceNames.add(node.id.name); + return; + } + + if (node.id.type !== 'ObjectPattern') return; + + for (const property of node.id.properties) { + if (property.type !== 'Property') continue; + const keyName = property.key.name ?? property.key.value; + if (property.value.type !== 'Identifier') continue; + const localName = property.value.name; + if (keyName === 'isKeyObject') { + isKeyObjectNames.add(localName); + } else if (keyObjectClassNames.has(keyName)) { + knownKeyObjectClassNames.add(localName); + } + } + return; + } + + if (node.id.type === 'Identifier' && isKeyObjectFactory(node.init)) { + knownKeyObjectNames.add(node.id.name); + } + }, + + MemberExpression(node) { + const property = getPropertyName(node); + const replacement = accessors.get(property); + if (replacement === undefined) return; + if (!isKnownKeyObject(node.object)) return; + + context.report({ + node: node.property, + messageId: 'noPublicAccessor', + data: { + property, + replacement, + }, + }); + }, + + }; + }, +};