From 5a064fdbd565d5a98f99144c020dd8bdfe0cb47f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Wed, 6 May 2026 15:49:06 +0200 Subject: [PATCH 1/9] crypto: improve accuracy of SubtleCrypto.supports Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63104 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- lib/internal/crypto/webcrypto.js | 42 ++++++++++++------- test/fixtures/webcrypto/supports-level-2.mjs | 30 ++++++++++++- .../webcrypto/supports-secure-curves.mjs | 10 ++++- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index 663ca798248ec3..f492054d8a6f0f 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -1928,7 +1928,11 @@ class SubtleCrypto { } } - return check(operation, algorithm, length); + try { + return check(operation, algorithm, length); + } catch { + return false; + } } } @@ -1971,25 +1975,35 @@ function check(op, alg, length) { return true; case 'deriveBits': { if (normalizedAlgorithm.name === 'HKDF') { - try { - require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length); } if (normalizedAlgorithm.name === 'PBKDF2') { - try { - require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); - } catch { - return false; - } + require('internal/crypto/pbkdf2').validatePbkdf2DeriveBitsLength(length); } if (StringPrototypeStartsWith(normalizedAlgorithm.name, 'Argon2')) { - try { - require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); - } catch { + require('internal/crypto/argon2').validateArgon2DeriveBitsLength(length); + } + + if (normalizedAlgorithm.name === 'X25519' && length > 256) { + return false; + } + + if (normalizedAlgorithm.name === 'X448' && length > 448) { + return false; + } + + if (normalizedAlgorithm.name === 'ECDH') { + const namedCurve = getCryptoKeyAlgorithm(normalizedAlgorithm.public).namedCurve; + const maxLength = { + '__proto__': null, + 'P-256': 256, + 'P-384': 384, + 'P-521': 528, + }[namedCurve]; + + if (length > maxLength) { return false; } } diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 51a3ff10ac2188..1850acacdc22e9 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -103,18 +103,36 @@ export const vectors = { [true, { name: 'X25519', public: X25519.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'X25519', public: X25519.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [true, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'X25519', public: X25519.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], [true, { name: 'X25519', public: X25519.publicKey }, 'HKDF'], [true, { name: 'ECDH', public: ECDH.publicKey }, { name: 'AES-CBC', length: 128 }], - [true, + [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 256 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 257 }], + [false, + { name: 'ECDH', public: ECDH.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 264 }], [true, { name: 'ECDH', public: ECDH.publicKey }, 'HKDF'], @@ -143,10 +161,18 @@ export const vectors = { [true, { name: 'ECDH', public: ECDH.publicKey }], + [true, + { name: 'ECDH', public: ECDH.publicKey }, 256], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 257], + [false, + { name: 'ECDH', public: ECDH.publicKey }, 264], [false, { name: 'ECDH', public: ECDH.privateKey }], [false, 'ECDH'], [true, { name: 'X25519', public: X25519.publicKey }], + [true, { name: 'X25519', public: X25519.publicKey }, 256], + [false, { name: 'X25519', public: X25519.publicKey }, 257], [false, { name: 'X25519', public: X25519.privateKey }], [false, 'X25519'], ], diff --git a/test/fixtures/webcrypto/supports-secure-curves.mjs b/test/fixtures/webcrypto/supports-secure-curves.mjs index e2799b5baf40fc..56bb89c28afaae 100644 --- a/test/fixtures/webcrypto/supports-secure-curves.mjs +++ b/test/fixtures/webcrypto/supports-secure-curves.mjs @@ -28,15 +28,23 @@ export const vectors = { [!boringSSL, { name: 'X448', public: X448?.publicKey }, { name: 'AES-CBC', length: 128 }], - [!boringSSL, + [false, { name: 'X448', public: X448?.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], + [!boringSSL, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 448 }], + [false, + { name: 'X448', public: X448?.publicKey }, + { name: 'HMAC', hash: 'SHA-256', length: 449 }], [!boringSSL, { name: 'X448', public: X448?.publicKey }, 'HKDF'], ], 'deriveBits': [ [!boringSSL, { name: 'X448', public: X448?.publicKey }], + [!boringSSL, { name: 'X448', public: X448?.publicKey }, 448], + [false, { name: 'X448', public: X448?.publicKey }, 449], [false, { name: 'X448', public: X25519.publicKey }], [false, { name: 'X448', public: X448?.privateKey }], [false, 'X448'], From cbcbcd72642428c4055744d9d8263fa8a8558c29 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 7 May 2026 21:02:13 +0200 Subject: [PATCH 2/9] crypto: reject invalid raw key imports Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63134 Reviewed-By: Anna Henningsen Reviewed-By: Yagiz Nizipli Reviewed-By: James M Snell --- lib/internal/crypto/ml_kem.js | 4 +- src/crypto/crypto_keys.cc | 121 ++++++++++++------ test/parallel/test-crypto-key-objects-raw.js | 27 +++- .../test-crypto-pqc-key-objects-slh-dsa.js | 6 + 4 files changed, 119 insertions(+), 39 deletions(-) diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index cc09c24048d60b..53b32f22a6b8f5 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -13,8 +13,8 @@ const { KEMDecapsulateJob, KEMEncapsulateJob, kKeyFormatDER, - kKeyFormatRawPrivate, kKeyFormatRawPublic, + kKeyFormatRawSeed, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, @@ -182,7 +182,7 @@ function mlKemImportKey( case 'raw-seed': { const isPublic = format === 'raw-public'; verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); - handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawPrivate, name); + handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } case 'jwk': { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 81451d88f7e4b7..dba6ae38dad3de 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -287,6 +287,85 @@ int GetNidFromName(const char* name) { return NID_undef; #endif } + +bool IsUnavailablePqcKeyType(Environment* env, Local key_type) { + return 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()); +} + +bool IsUnsupportedRawKeyType(Environment* env, Local key_type) { + return 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()); +} + +void ValidateRawKeyImportFormat(Environment* env, + Local key_type, + const char* key_type_name, + int id, + EVPKeyPointer::PKFormatType format) { + auto validate_raw_format = + [&](EVPKeyPointer::PKFormatType expected_private_format) { + if (format == EVPKeyPointer::PKFormatType::RAW_PUBLIC || + format == expected_private_format) { + return; + } + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + }; + + if (key_type->StringEquals(env->crypto_ec_string())) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + } + + switch (id) { + case EVP_PKEY_X25519: + case EVP_PKEY_X448: + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + default: + break; + } + +#if OPENSSL_WITH_PQC + if (IsPqcSeedKeyId(id)) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_SEED); + } + if (IsPqcRawPrivateKeyId(id)) { + return validate_raw_format(EVPKeyPointer::PKFormatType::RAW_PRIVATE); + } +#endif + + if (IsUnavailablePqcKeyType(env, key_type)) { + THROW_ERR_INVALID_ARG_VALUE(env, "Unsupported key type"); + return; + } + + if (IsUnsupportedRawKeyType(env, key_type)) { + THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); + return; + } + + THROW_ERR_INVALID_ARG_VALUE( + env, "Invalid asymmetricKeyType: %s", key_type_name); +} } // namespace bool KeyObjectData::ToEncodedPublicKey( @@ -492,6 +571,12 @@ static KeyObjectData ImportRawKey(Environment* env, } }; + const int id = GetNidFromName(key_type_name); + ValidateRawKeyImportFormat(env, key_type, key_type_name, id, format); + if (env->isolate()->HasPendingException()) { + return {}; + } + // EC keys if (key_type->StringEquals(env->crypto_ec_string())) { int curve_nid = ncrypto::Ec::GetCurveIdFromName(named_curve); @@ -549,8 +634,6 @@ static KeyObjectData ImportRawKey(Environment* env, 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; @@ -589,40 +672,6 @@ static KeyObjectData ImportRawKey(Environment* env, 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 {}; } diff --git a/test/parallel/test-crypto-key-objects-raw.js b/test/parallel/test-crypto-key-objects-raw.js index e0871c2f0cd3fd..e8d5aced4e22cb 100644 --- a/test/parallel/test-crypto-key-objects-raw.js +++ b/test/parallel/test-crypto-key-objects-raw.js @@ -154,7 +154,11 @@ if (hasOpenSSL(3, 5)) { assert.throws(() => privKeyObj.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); - for (const format of ['raw-public', 'raw-private', 'raw-seed']) { + assert.throws(() => crypto.createPrivateKey({ + key: Buffer.alloc(32), format: 'raw-public', asymmetricKeyType: 'dh', + }), { code: 'ERR_INVALID_ARG_VALUE' }); + + for (const format of ['raw-private', 'raw-seed']) { assert.throws(() => crypto.createPrivateKey({ key: Buffer.alloc(32), format, asymmetricKeyType: 'dh', }), { @@ -326,6 +330,12 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey('ec_p256_private.pem', 'ascii')); assert.throws(() => ecPriv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: ecPriv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: 'ec', + namedCurve: 'P-256', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); if (process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported ed448/x448 test cases'); @@ -337,6 +347,11 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey(`${type}_private.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: priv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: type, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } if (hasOpenSSL(3, 5)) { @@ -344,6 +359,11 @@ if (hasOpenSSL(3, 5)) { fixtures.readKey('slh_dsa_sha2_128f_private.pem', 'ascii')); assert.throws(() => slhPriv.export({ format: 'raw-seed' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: slhPriv.export({ format: 'raw-private' }), + format: 'raw-seed', + asymmetricKeyType: 'slh-dsa-sha2-128f', + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } } @@ -354,6 +374,11 @@ if (hasOpenSSL(3, 5) || process.features.openssl_is_boringssl) { fixtures.readKey(`${type.replaceAll('-', '_')}_private_seed_only.pem`, 'ascii')); assert.throws(() => priv.export({ format: 'raw-private' }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => crypto.createPrivateKey({ + key: priv.export({ format: 'raw-seed' }), + format: 'raw-private', + asymmetricKeyType: type, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } } 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 4cc8646cd8b90e..4a143a77d6a3c1 100644 --- a/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js +++ b/test/parallel/test-crypto-pqc-key-objects-slh-dsa.js @@ -91,6 +91,12 @@ for (const asymmetricKeyType of [ key: rawPriv, format: 'raw-private', asymmetricKeyType, }); assert.strictEqual(importedPriv.equals(key), true); + assert.throws(() => createPrivateKey({ + key: rawPriv, format: 'raw-seed', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); + assert.throws(() => createPublicKey({ + key: rawPriv, format: 'raw-seed', asymmetricKeyType, + }), { code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS' }); } if (!hasOpenSSL(3, 5)) { From d72b7e7851ee930bda4c66135d6ade55f0c11a9a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 25 May 2026 09:54:33 +0200 Subject: [PATCH 3/9] crypto: coerce -0 to +0 before native calls Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63556 Reviewed-By: Jordan Harband Reviewed-By: Chemi Atlow --- lib/internal/crypto/cipher.js | 13 +- lib/internal/crypto/diffiehellman.js | 5 +- lib/internal/crypto/hash.js | 9 +- lib/internal/crypto/hkdf.js | 2 + lib/internal/crypto/keygen.js | 30 +++- lib/internal/crypto/random.js | 8 +- test/parallel/test-crypto-negative-zero.js | 160 +++++++++++++++++++++ 7 files changed, 213 insertions(+), 14 deletions(-) create mode 100644 test/parallel/test-crypto-negative-zero.js diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index 1aeec8275d7b2d..d9eef29c5f71dc 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -108,7 +108,8 @@ function getUIntOption(options, key) { if (options && (value = options[key]) != null) { if (value >>> 0 !== value) throw new ERR_INVALID_ARG_VALUE(`options.${key}`, value); - return value; + // Coerce -0 to +0. + return value + 0; } return -1; } @@ -265,10 +266,16 @@ function getCipherInfo(nameOrNid, options) { if (options !== undefined) { validateObject(options, 'options'); ({ keyLength, ivLength } = options); - if (keyLength !== undefined) + if (keyLength !== undefined) { validateInt32(keyLength, 'options.keyLength'); - if (ivLength !== undefined) + // Coerce -0 to +0. + keyLength += 0; + } + if (ivLength !== undefined) { validateInt32(ivLength, 'options.ivLength'); + // Coerce -0 to +0. + ivLength += 0; + } } const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength); diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 876b64077a2aae..12e5c98c27199c 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -98,8 +98,11 @@ function DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding) { // rejected with ERR_OSSL_BN_BITS_TOO_SMALL) by OpenSSL. The glue code // in node_crypto.cc accepts values that are IsInt32() for that reason // and that's why we do that here too. - if (typeof sizeOrKey === 'number') + if (typeof sizeOrKey === 'number') { validateInt32(sizeOrKey, 'sizeOrKey'); + // Coerce -0 to +0. + sizeOrKey += 0; + } if (keyEncoding && !Buffer.isEncoding(keyEncoding) && keyEncoding !== 'buffer') { diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 7e8561f4b344c8..f8bb7f66b8cb60 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -95,10 +95,13 @@ function Hash(algorithm, options) { const isCopy = algorithm instanceof _Hash; if (!isCopy) validateString(algorithm, 'algorithm'); - const xofLen = typeof options === 'object' && options !== null ? + let xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; - if (xofLen !== undefined) + if (xofLen !== undefined) { validateUint32(xofLen, 'options.outputLength'); + // Coerce -0 to +0. + xofLen += 0; + } // Lookup the cached ID from JS land because it's faster than decoding // the string in C++ land. const algorithmId = isCopy ? -1 : getCachedHashId(algorithm); @@ -288,6 +291,8 @@ function hash(algorithm, input, options) { if (outputLength !== undefined) { validateUint32(outputLength, 'outputLength'); + // Coerce -0 to +0. + outputLength += 0; } if (outputLength === undefined) { diff --git a/lib/internal/crypto/hkdf.js b/lib/internal/crypto/hkdf.js index 73b16da6923024..c9b868e23af4e2 100644 --- a/lib/internal/crypto/hkdf.js +++ b/lib/internal/crypto/hkdf.js @@ -58,6 +58,8 @@ const validateParameters = hideStackFrames((hash, key, salt, info, length) => { info = validateByteSource.withoutStackTrace(info, 'info'); validateInteger.withoutStackTrace(length, 'length', 0, kMaxLength); + // Coerce -0 to +0. + length += 0; if (info.byteLength > 1024) { throw new ERR_OUT_OF_RANGE.HideStackFramesError( diff --git a/lib/internal/crypto/keygen.js b/lib/internal/crypto/keygen.js index 515a31141913df..553aca2daa4877 100644 --- a/lib/internal/crypto/keygen.js +++ b/lib/internal/crypto/keygen.js @@ -219,14 +219,18 @@ function createJob(mode, type, options) { case 'rsa-pss': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { publicExponent } = options; if (publicExponent == null) { publicExponent = 0x10001; } else { validateUint32(publicExponent, 'options.publicExponent'); + // Coerce -0 to +0. + publicExponent += 0; } if (type === 'rsa') { @@ -239,11 +243,15 @@ function createJob(mode, type, options) { } const { - hash, mgf1Hash, hashAlgorithm, mgf1HashAlgorithm, saltLength, + hash, mgf1Hash, hashAlgorithm, mgf1HashAlgorithm, } = options; + let { saltLength } = options; - if (saltLength !== undefined) + if (saltLength !== undefined) { validateInt32(saltLength, 'options.saltLength', 0); + // Coerce -0 to +0. + saltLength += 0; + } if (hashAlgorithm !== undefined) validateString(hashAlgorithm, 'options.hashAlgorithm'); if (mgf1HashAlgorithm !== undefined) @@ -284,14 +292,19 @@ function createJob(mode, type, options) { case 'dsa': { validateObject(options, 'options'); - const { modulusLength } = options; + let { modulusLength } = options; validateUint32(modulusLength, 'options.modulusLength'); + // Coerce -0 to +0. + modulusLength += 0; let { divisorLength } = options; if (divisorLength == null) { divisorLength = -1; - } else + } else { validateInt32(divisorLength, 'options.divisorLength', 0); + // Coerce -0 to +0. + divisorLength += 0; + } return new DsaKeyPairGenJob( mode, @@ -321,7 +334,8 @@ function createJob(mode, type, options) { case 'dh': { validateObject(options, 'options'); - const { group, primeLength, prime, generator } = options; + const { group, prime } = options; + let { primeLength, generator } = options; if (group != null) { if (prime != null) throw new ERR_INCOMPATIBLE_OPTION_PAIR('group', 'prime'); @@ -342,6 +356,8 @@ function createJob(mode, type, options) { validateBuffer(prime, 'options.prime'); } else if (primeLength != null) { validateInt32(primeLength, 'options.primeLength', 0); + // Coerce -0 to +0. + primeLength += 0; } else { throw new ERR_MISSING_OPTION( 'At least one of the group, prime, or primeLength options'); @@ -349,6 +365,8 @@ function createJob(mode, type, options) { if (generator != null) { validateInt32(generator, 'options.generator', 0); + // Coerce -0 to +0. + generator += 0; } return new DhKeyPairGenJob( mode, diff --git a/lib/internal/crypto/random.js b/lib/internal/crypto/random.js index b4bfcb6c0a05de..339fbb7066528c 100644 --- a/lib/internal/crypto/random.js +++ b/lib/internal/crypto/random.js @@ -603,12 +603,14 @@ function checkPrime(candidate, options = kEmptyObject, callback) { } validateFunction(callback, 'callback'); validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobAsync, candidate, checks); job.ondone = callback; @@ -632,12 +634,14 @@ function checkPrimeSync(candidate, options = kEmptyObject) { ); } validateObject(options, 'options'); - const { + let { checks = 0, } = options; // The checks option is unsigned but must fit into a signed C int for OpenSSL. validateInt32(checks, 'options.checks', 0); + // Coerce -0 to +0. + checks += 0; const job = new CheckPrimeJob(kCryptoJobSync, candidate, checks); const { 0: err, 1: result } = job.run(); diff --git a/test/parallel/test-crypto-negative-zero.js b/test/parallel/test-crypto-negative-zero.js new file mode 100644 index 00000000000000..0af9220569c374 --- /dev/null +++ b/test/parallel/test-crypto-negative-zero.js @@ -0,0 +1,160 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); + +function getOutcome(fn) { + try { + return { result: fn() }; + } catch (err) { + return { err }; + } +} + +function assertSameOutcome(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.deepStrictEqual(actual.result, expected.result); + } +} + +function assertSameErrorOrSuccess(actual, expected) { + if (expected.err !== undefined) { + assert(actual.err instanceof Error); + assert.strictEqual(actual.err.name, expected.err.name); + assert.strictEqual(actual.err.code, expected.err.code); + assert.strictEqual(actual.err.message, expected.err.message); + } else { + assert.strictEqual(actual.err, undefined); + } +} + +{ + const expected = getOutcome(() => + crypto.hkdfSync('sha256', 'key', 'salt', 'info', 0), + ); + assertSameOutcome( + getOutcome(() => crypto.hkdfSync('sha256', 'key', 'salt', 'info', -0)), + expected, + ); + crypto.hkdf('sha256', 'key', 'salt', 'info', -0, + common.mustCall((err, result) => { + assertSameOutcome({ err, result }, expected); + })); +} + +{ + assert.strictEqual( + crypto.checkPrimeSync(Buffer.from([3]), { checks: -0 }), + true, + ); + crypto.checkPrime(Buffer.from([3]), { checks: -0 }, + common.mustSucceed((result) => { + assert.strictEqual(result, true); + })); +} + +{ + assert.throws(() => crypto.createDiffieHellman(-0, 2), { + name: 'Error', + }); +} + +{ + for (const [type, getOptions] of [ + ['rsa', (zero) => ({ modulusLength: zero })], + ['rsa', (zero) => ({ modulusLength: 512, publicExponent: zero })], + ['rsa-pss', (zero) => ({ + modulusLength: 512, + publicExponent: 65537, + saltLength: zero, + })], + ['dsa', (zero) => ({ modulusLength: zero })], + ['dh', (zero) => ({ primeLength: zero })], + ['dh', (zero) => ({ primeLength: 2, generator: zero })], + ]) { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(-0))), + getOutcome(() => crypto.generateKeyPairSync(type, getOptions(0))), + ); + } + + if (!hasOpenSSL(3)) { + common.printSkipMessage( + 'Skipping DSA divisorLength 0 key generation on OpenSSL 1.1.1'); + } else { + assertSameErrorOrSuccess( + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: -0, + })), + getOutcome(() => crypto.generateKeyPairSync('dsa', { + modulusLength: 512, + divisorLength: 0, + })), + ); + } + + crypto.generateKeyPair('rsa', { modulusLength: -0 }, + common.mustCall((err) => { + assert(err instanceof Error); + })); +} + +if (!process.features.openssl_is_boringssl) { + assert.strictEqual( + crypto.createHash('shake128', { outputLength: -0 }).digest('hex'), + '', + ); + assert.strictEqual( + crypto.createHash('shake128', { outputLength: 5 }) + .copy({ outputLength: -0 }) + .digest('hex'), + '', + ); + assert.strictEqual( + crypto.hash('shake128', 'data', { outputLength: -0 }), + '', + ); +} + +{ + const key = Buffer.alloc(16); + const iv = Buffer.alloc(12); + + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: -0 })), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv, { authTagLength: 0 })), + ); + assertSameErrorOrSuccess( + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: -0 }, + )), + getOutcome(() => crypto.createCipheriv( + 'aes-128-gcm', key, iv).setAAD( + Buffer.alloc(0), + { plaintextLength: 0 }, + )), + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { keyLength: -0 }), + undefined, + ); + assert.strictEqual( + crypto.getCipherInfo('aes-128-cbc', { ivLength: -0 }), + undefined, + ); +} From 12c35abe46406e5e8d4de07c0b091d7549cf9325 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 9 Jun 2026 21:48:16 +0200 Subject: [PATCH 4/9] crypto: refactor keyObject.toCryptoKey() and SubtleCrypto.getPublicKey() Move KeyObject.prototype.toCryptoKey() onto the base KeyObject class and dispatch from the cached native key type. Both secret and asymmetric conversions now pass a KeyObjectHandle through the Web Crypto import paths. Expose KeyObjectHandle.prototype.getKeyType() so asymmetric importers can validate public/private usages without wrapping the handle back into a KeyObject. Secret importers likewise consume KeyObjectHandle directly. Use the shared asymmetric conversion helper to derive public CryptoKeys for SubtleCrypto.getPublicKey(), avoiding the temporary PrivateKeyObject/createPublicKey round trip while keeping usage validation in the import path. Update getPublicKey and KeyObject.toCryptoKey tests to be driven from the Web Crypto supported-algorithm registry so new algorithms require either coverage or an explicit skip. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63622 Reviewed-By: Yagiz Nizipli --- lib/internal/crypto/aes.js | 8 +- lib/internal/crypto/cfrg.js | 10 +- lib/internal/crypto/chacha20_poly1305.js | 5 +- lib/internal/crypto/ec.js | 9 +- lib/internal/crypto/keys.js | 319 ++++++----- lib/internal/crypto/mac.js | 8 +- lib/internal/crypto/ml_dsa.js | 10 +- lib/internal/crypto/ml_kem.js | 10 +- lib/internal/crypto/rsa.js | 10 +- lib/internal/crypto/webcrypto.js | 8 +- src/crypto/crypto_keys.cc | 10 + src/crypto/crypto_keys.h | 1 + .../test-crypto-key-objects-to-crypto-key.js | 517 +++++++++++------- .../test-webcrypto-get-public-key.mjs | 97 +++- 14 files changed, 630 insertions(+), 392 deletions(-) diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index 981502c51700be..dd7c5ac5fd247a 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -40,8 +40,6 @@ const { InternalCryptoKey, getCryptoKeyAlgorithm, getCryptoKeyHandle, - getKeyObjectHandle, - getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); const { @@ -223,10 +221,10 @@ function aesImportKey( let handle; let length; switch (format) { - case 'KeyObject': { - length = getKeyObjectSymmetricKeySize(keyData) * 8; + case 'KeyObjectHandle': { + length = keyData.getSymmetricKeySize() * 8; validateKeyLength(length); - handle = getKeyObjectHandle(keyData); + handle = keyData; break; } case 'raw-secret': diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 97660f87a271b0..1099466eec3ebb 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -13,6 +13,7 @@ const { kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatPKCS8, @@ -39,8 +40,6 @@ const { const { getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -178,12 +177,11 @@ function cfrgImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { + case 'KeyObjectHandle': verifyAcceptableCfrgKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + handle = keyData; break; - } case 'spki': { verifyAcceptableCfrgKeyUse(name, true, usagesSet); handle = importDerKey(keyData, true); diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 689cab59f3fbf2..8baaf6f981df21 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -23,7 +23,6 @@ const { const { InternalCryptoKey, getCryptoKeyHandle, - getKeyObjectHandle, } = require('internal/crypto/keys'); const { @@ -90,8 +89,8 @@ function c20pImportKey( let handle; switch (format) { - case 'KeyObject': { - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + handle = keyData; break; } case 'raw-secret': { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index d102b3fe05a29c..94040cc7f71813 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -46,8 +46,6 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, } = require('internal/crypto/keys'); const { @@ -185,12 +183,11 @@ function ecImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { + case 'KeyObjectHandle': verifyAcceptableEcKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + handle = keyData; break; - } case 'spki': { verifyAcceptableEcKeyUse(name, true, usagesSet); handle = importDerKey(keyData, true); diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index 2722ecc0520e2c..9c6c238c00e193 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -143,6 +143,8 @@ const { 2: PublicKeyObject, 3: PrivateKeyObject, } = createNativeKeyObjectClass((NativeKeyObject) => { + let webidl; + // Publicly visible KeyObject class. class KeyObject extends NativeKeyObject { #slots; @@ -160,6 +162,32 @@ const { return getKeyObjectType(this); } + toCryptoKey(algorithm, extractable, keyUsages) { + webidl ??= require('internal/crypto/webidl'); + algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); + extractable = webidl.converters.boolean(extractable); + keyUsages = webidl.converters['sequence'](keyUsages); + + const type = getKeyObjectType(this); + const handle = getKeyObjectHandle(this); + switch (type) { + case 'secret': + return toCryptoKeySecret( + handle, + algorithm, + extractable, + keyUsages); + case 'public': + // Fall through + case 'private': + return toCryptoKey( + handle, + algorithm, + extractable, + keyUsages); + } + } + static from(key) { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); @@ -210,8 +238,6 @@ const { }, }); - let webidl; - class SecretKeyObject extends KeyObject { constructor(handle) { super('secret', handle); @@ -233,67 +259,6 @@ const { } return handle.export(); } - - toCryptoKey(algorithm, extractable, keyUsages) { - webidl ??= require('internal/crypto/webidl'); - algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); - extractable = webidl.converters.boolean(extractable); - keyUsages = webidl.converters['sequence'](keyUsages); - - let result; - switch (algorithm.name) { - case 'HMAC': - // Fall through - case 'KMAC128': - // Fall through - case 'KMAC256': - result = require('internal/crypto/mac') - .macImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'AES-CTR': - // Fall through - case 'AES-CBC': - // Fall through - case 'AES-GCM': - // Fall through - case 'AES-KW': - // Fall through - case 'AES-OCB': - result = require('internal/crypto/aes') - .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); - break; - case 'ChaCha20-Poly1305': - result = require('internal/crypto/chacha20_poly1305') - .c20pImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); - break; - case 'HKDF': - // Fall through - case 'PBKDF2': - // Fall through - case 'Argon2d': - // Fall through - case 'Argon2i': - // Fall through - case 'Argon2id': - result = importGenericSecretKey( - algorithm, - 'KeyObject', - this, - extractable, - keyUsages); - break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); - } - - if (getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, - 'SyntaxError'); - } - - return result; - } } class AsymmetricKeyObject extends KeyObject { @@ -304,68 +269,6 @@ const { get asymmetricKeyDetails() { return { ...getKeyObjectAsymmetricKeyDetails(this) }; } - - toCryptoKey(algorithm, extractable, keyUsages) { - webidl ??= require('internal/crypto/webidl'); - algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); - extractable = webidl.converters.boolean(extractable); - keyUsages = webidl.converters['sequence'](keyUsages); - - let result; - switch (algorithm.name) { - case 'RSASSA-PKCS1-v1_5': - // Fall through - case 'RSA-PSS': - // Fall through - case 'RSA-OAEP': - result = require('internal/crypto/rsa') - .rsaImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ECDSA': - // Fall through - case 'ECDH': - result = require('internal/crypto/ec') - .ecImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'Ed25519': - // Fall through - case 'Ed448': - // Fall through - case 'X25519': - // Fall through - case 'X448': - result = require('internal/crypto/cfrg') - .cfrgImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ML-DSA-44': - // Fall through - case 'ML-DSA-65': - // Fall through - case 'ML-DSA-87': - result = require('internal/crypto/ml_dsa') - .mlDsaImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - case 'ML-KEM-512': - // Fall through - case 'ML-KEM-768': - // Fall through - case 'ML-KEM-1024': - result = require('internal/crypto/ml_kem') - .mlKemImportKey('KeyObject', this, algorithm, extractable, keyUsages); - break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); - } - - const resultType = getCryptoKeyType(result); - if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { - throw lazyDOMException( - `Usages cannot be empty when importing a ${resultType} key.`, - 'SyntaxError'); - } - - return result; - } } class PublicKeyObject extends AsymmetricKeyObject { @@ -778,6 +681,167 @@ function createPublicKey(key) { return new PublicKeyObject(handle); } +/** + * Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the + * algorithm-specific Web Crypto import path. + * @param {KeyObjectHandle} keyData + * @param {object} algorithm + * @param {boolean} extractable + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toCryptoKeySecret( + keyData, + algorithm, + extractable, + keyUsages, +) { + let result; + switch (algorithm.name) { + case 'HMAC': + // Fall through + case 'KMAC128': + // Fall through + case 'KMAC256': + result = require('internal/crypto/mac') + .macImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'AES-CTR': + // Fall through + case 'AES-CBC': + // Fall through + case 'AES-GCM': + // Fall through + case 'AES-KW': + // Fall through + case 'AES-OCB': + result = require('internal/crypto/aes') + .aesImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); + break; + case 'ChaCha20-Poly1305': + result = require('internal/crypto/chacha20_poly1305') + .c20pImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); + break; + case 'HKDF': + // Fall through + case 'PBKDF2': + // Fall through + case 'Argon2d': + // Fall through + case 'Argon2i': + // Fall through + case 'Argon2id': + result = importGenericSecretKey( + algorithm, + 'KeyObjectHandle', + keyData, + extractable, + keyUsages); + break; + default: + throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + } + + if (getCryptoKeyUsagesMask(result) === 0) { + throw lazyDOMException( + `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, + 'SyntaxError'); + } + + return result; +} + +/** + * Converts an asymmetric KeyObjectHandle to a CryptoKey by dispatching to + * the algorithm-specific Web Crypto import path. This preserves the same + * algorithm and usage validation used by SubtleCrypto.importKey(). + * @param {KeyObjectHandle} keyData + * @param {object} algorithm + * @param {boolean} extractable + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toCryptoKey( + keyData, + algorithm, + extractable, + keyUsages, +) { + let result; + switch (algorithm.name) { + case 'RSASSA-PKCS1-v1_5': + // Fall through + case 'RSA-PSS': + // Fall through + case 'RSA-OAEP': + result = require('internal/crypto/rsa') + .rsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ECDSA': + // Fall through + case 'ECDH': + result = require('internal/crypto/ec') + .ecImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'Ed25519': + // Fall through + case 'Ed448': + // Fall through + case 'X25519': + // Fall through + case 'X448': + result = require('internal/crypto/cfrg') + .cfrgImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ML-DSA-44': + // Fall through + case 'ML-DSA-65': + // Fall through + case 'ML-DSA-87': + result = require('internal/crypto/ml_dsa') + .mlDsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + case 'ML-KEM-512': + // Fall through + case 'ML-KEM-768': + // Fall through + case 'ML-KEM-1024': + result = require('internal/crypto/ml_kem') + .mlKemImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); + break; + default: + throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + } + + const resultType = getCryptoKeyType(result); + if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { + throw lazyDOMException( + `Usages cannot be empty when importing a ${resultType} key.`, + 'SyntaxError'); + } + + return result; +} + +/** + * Derives a public CryptoKey from a private CryptoKey. The resulting key uses + * the private key's algorithm, is extractable, and validates the requested + * public usages through the shared asymmetric import path. + * @param {CryptoKey} key + * @param {string[]} keyUsages + * @returns {CryptoKey} + */ +function toPublicCryptoKey(key, keyUsages) { + const handle = new KeyObjectHandle(); + handle.init(kKeyTypePublic, getCryptoKeyHandle(key), + null, null, null, null); + return toCryptoKey( + handle, + getCryptoKeyAlgorithm(key), + true, + keyUsages); +} + function createPrivateKey(key) { const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePrivate); @@ -1204,8 +1268,8 @@ function importGenericSecretKey( let handle; switch (format) { - case 'KeyObject': { - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + handle = keyData; break; } case 'raw-secret': @@ -1238,6 +1302,7 @@ module.exports = { parsePublicKeyEncoding, parsePrivateKeyEncoding, parseKeyEncoding, + toPublicCryptoKey, preparePrivateKey, preparePublicOrPrivateKey, prepareSecretKey, diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 724b2104d4b8c8..5c6955d22b1904 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -30,8 +30,6 @@ const { InternalCryptoKey, getCryptoKeyAlgorithm, getCryptoKeyHandle, - getKeyObjectHandle, - getKeyObjectSymmetricKeySize, } = require('internal/crypto/keys'); const { @@ -114,9 +112,9 @@ function macImportKey( let handle; let length; switch (format) { - case 'KeyObject': { - length = getKeyObjectSymmetricKeySize(keyData) * 8; - handle = getKeyObjectHandle(keyData); + case 'KeyObjectHandle': { + length = keyData.getSymmetricKeySize() * 8; + handle = keyData; break; } case 'raw-secret': diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 26eb2e51e9bb6f..196976223a8d3b 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -16,6 +16,7 @@ const { kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatRaw, @@ -42,8 +43,6 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -150,12 +149,11 @@ function mlDsaImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { + case 'KeyObjectHandle': verifyAcceptableMlDsaKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + handle = keyData; break; - } case 'spki': { verifyAcceptableMlDsaKeyUse(name, true, usagesSet); handle = importDerKey(keyData, true); diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index 53b32f22a6b8f5..ef3a45c56aaeee 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -15,6 +15,7 @@ const { kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, + kKeyTypePublic, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, @@ -39,8 +40,6 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -149,12 +148,11 @@ function mlKemImportKey( let handle; const usagesSet = new SafeSet(usages); switch (format) { - case 'KeyObject': { + case 'KeyObjectHandle': verifyAcceptableMlKemKeyUse( - name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + handle = keyData; break; - } case 'spki': { verifyAcceptableMlKemKeyUse(name, true, usagesSet); handle = importDerKey(keyData, true); diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index a09dd7b9f0fda9..0442f8a80db0d4 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -12,6 +12,7 @@ const { SignJob, kCryptoJobWebCrypto, kKeyFormatDER, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kKeyVariantRSA_OAEP, @@ -47,8 +48,6 @@ const { getCryptoKeyAlgorithm, getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, } = require('internal/crypto/keys'); const { @@ -217,12 +216,11 @@ function rsaImportKey( const usagesSet = new SafeSet(usages); let handle; switch (format) { - case 'KeyObject': { + case 'KeyObjectHandle': verifyAcceptableRsaKeyUse( - algorithm.name, getKeyObjectType(keyData) === 'public', usagesSet); - handle = getKeyObjectHandle(keyData); + algorithm.name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + handle = keyData; break; - } case 'spki': { verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet); handle = importDerKey(keyData, true); diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index f492054d8a6f0f..f40557367951e0 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -44,7 +44,6 @@ const { } = require('internal/errors'); const { - createPublicKey, CryptoKey, getCryptoKeyAlgorithm, getCryptoKeyExtractable, @@ -54,7 +53,7 @@ const { getCryptoKeyUsagesMask, hasCryptoKeyUsage, importGenericSecretKey, - PrivateKeyObject, + toPublicCryptoKey, } = require('internal/crypto/keys'); const { @@ -1463,10 +1462,7 @@ function getPublicKeyImpl(key, keyUsages) { throw lazyDOMException('key must be a private key', type === 'secret' ? 'NotSupportedError' : 'InvalidAccessError'); - // 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, usages); + return toPublicCryptoKey(key, usages); } function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index dba6ae38dad3de..bdc9aad1cad4e1 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1022,6 +1022,7 @@ Local KeyObjectHandle::Initialize(Environment* env) { KeyObjectHandle::kInternalFieldCount); SetProtoMethod(isolate, templ, "init", Init); + SetProtoMethodNoSideEffect(isolate, templ, "getKeyType", GetKeyType); SetProtoMethodNoSideEffect( isolate, templ, "getSymmetricKeySize", GetSymmetricKeySize); SetProtoMethodNoSideEffect( @@ -1049,6 +1050,7 @@ void KeyObjectHandle::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Init); + registry->Register(GetKeyType); registry->Register(GetSymmetricKeySize); registry->Register(GetAsymmetricKeyType); registry->Register(CheckEcKeyData); @@ -1179,6 +1181,14 @@ void KeyObjectHandle::Init(const FunctionCallbackInfo& args) { } } +void KeyObjectHandle::GetKeyType(const FunctionCallbackInfo& args) { + KeyObjectHandle* key; + ASSIGN_OR_RETURN_UNWRAP(&key, args.This()); + + args.GetReturnValue().Set( + Uint32::NewFromUnsigned(args.GetIsolate(), key->Data().GetKeyType())); +} + void KeyObjectHandle::Equals(const FunctionCallbackInfo& args) { KeyObjectHandle* self_handle; KeyObjectHandle* arg_handle; diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h index b91a69bb048783..49714bdb75e434 100644 --- a/src/crypto/crypto_keys.h +++ b/src/crypto/crypto_keys.h @@ -151,6 +151,7 @@ class KeyObjectHandle : public BaseObject { static void New(const v8::FunctionCallbackInfo& args); static void Init(const v8::FunctionCallbackInfo& args); + static void GetKeyType(const v8::FunctionCallbackInfo& args); static void GetKeyDetail(const v8::FunctionCallbackInfo& args); static void Equals(const v8::FunctionCallbackInfo& args); 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 5c3148647324b0..66a1a18955fca6 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -1,11 +1,10 @@ 'use strict'; +// Flags: --expose-internals const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); -const { hasOpenSSL } = require('../common/crypto'); - const assert = require('assert'); const { createSecretKey, @@ -13,6 +12,29 @@ const { randomBytes, generateKeyPairSync, } = require('crypto'); +const { kSupportedAlgorithms } = require('internal/crypto/util'); + +const hashes = Object.keys(kSupportedAlgorithms.digest).filter((name) => { + return name.startsWith('SHA-') || name.startsWith('SHA3-'); +}); +const namedCurves = ['P-256', 'P-384', 'P-521']; + +const signingUsages = { + public: ['verify'], + private: ['sign'], +}; +const derivationUsages = { + public: [], + private: ['deriveBits'], +}; +const rsaOaepUsages = { + public: ['encrypt', 'wrapKey'], + private: ['decrypt', 'unwrapKey'], +}; +const mlKemUsages = { + public: ['encapsulateBits'], + private: ['decapsulateBits'], +}; function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { assert.strictEqual(cryptoKey instanceof CryptoKey, true); @@ -23,229 +45,336 @@ function assertCryptoKey(cryptoKey, keyObject, algorithm, extractable, usages) { assert.strictEqual(keyObject.equals(KeyObject.from(cryptoKey)), true); } -{ +function algorithmName(algorithm) { + return typeof algorithm === 'string' ? algorithm : algorithm.name; +} + +function getUsages(keyObject, usages) { + return keyObject.type === 'public' ? usages.public : usages.private; +} + +function secretKey(length) { + return createSecretKey(randomBytes(length >> 3)); +} + +function vector( + keyObject, + algorithm, + usages, + expected, + extractable, +) { + return { + keyObject, + algorithm, + usages, + expected, + extractable, + }; +} + +function assertVector(vector, extractable) { + const { keyObject, algorithm, usages, expected } = vector; + const name = algorithmName(algorithm); + + const cryptoKey = keyObject.toCryptoKey(algorithm, extractable, usages); + assertCryptoKey(cryptoKey, keyObject, name, extractable, usages); + if (expected !== undefined && 'length' in expected) + assert.strictEqual(cryptoKey.algorithm.length, expected.length); + if (expected !== undefined && 'hash' in expected) + assert.strictEqual(cryptoKey.algorithm.hash.name, expected.hash); + if (expected !== undefined && 'namedCurve' in expected) + assert.strictEqual(cryptoKey.algorithm.namedCurve, expected.namedCurve); +} + +function runVector(vector) { + if (vector.extractable !== undefined) { + assertVector(vector, vector.extractable); + return; + } + assertVector(vector, true); + assertVector(vector, false); +} + +function symmetricVectors(name, usages) { + const vectors = []; for (const length of [128, 192, 256]) { - const key = createSecretKey(randomBytes(length >> 3)); - 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]) { - const cryptoKey = key.toCryptoKey(algorithm, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, algorithm !== 'ChaCha20-Poly1305' ? length : undefined); - } - } + vectors.push(vector( + secretKey(length), + name, + usages, + { length })); } + return vectors; } -{ - const pbkdf2 = createSecretKey(randomBytes(16)); - const algorithm = 'PBKDF2'; - const usages = ['deriveBits']; - assert.throws(() => pbkdf2.toCryptoKey(algorithm, true, usages), { - name: 'SyntaxError', - message: 'PBKDF2 keys are not extractable' +function chacha20Poly1305Vectors() { + return [ + vector( + secretKey(256), + 'ChaCha20-Poly1305', + ['encrypt', 'decrypt'], + { length: undefined }), + ]; +} + +function genericSecretVectors(name) { + return [ + vector( + createSecretKey(randomBytes(16)), + name, + ['deriveBits'], + undefined, + false), + ]; +} + +function macInvalid(algorithm, invalidLengthMessage) { + const key = createSecretKey(randomBytes(32)); + const usages = ['sign', 'verify']; + + assert.throws(() => { + createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); + }, { + name: 'DataError', + message: 'Zero-length key is not supported', }); - assert.throws(() => pbkdf2.toCryptoKey(algorithm, false, ['wrapKey']), { + + assert.throws(() => key.toCryptoKey(algorithm, true, []), { name: 'SyntaxError', - message: 'Unsupported key usage for a PBKDF2 key' + message: 'Usages cannot be empty when importing a secret key.' + }); + + assert.throws(() => { + key.toCryptoKey({ ...algorithm, length: 0 }, true, usages); + }, { + name: 'DataError', + message: invalidLengthMessage, }); - const cryptoKey = pbkdf2.toCryptoKey(algorithm, false, usages); - assertCryptoKey(cryptoKey, pbkdf2, algorithm, false, usages); - assert.strictEqual(cryptoKey.algorithm.length, undefined); } -{ +function hmacVectors() { + const vectors = []; for (const length of [128, 192, 256]) { - const hmac = createSecretKey(randomBytes(length >> 3)); - const algorithm = 'HMAC'; - const usages = ['sign', 'verify']; - - assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm, hash: 'SHA-256' }, true, usages); - }, { - name: 'DataError', - message: 'Zero-length key is not supported', - }); - - assert.throws(() => { - hmac.toCryptoKey({ - name: algorithm, - hash: 'SHA-256', - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a secret key.' - }); - - for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) { - for (const extractable of [true, false]) { - assert.throws(() => { - hmac.toCryptoKey({ name: algorithm, hash: 'SHA-256', length: 0 }, true, usages); - }, { - name: 'DataError', - message: 'HmacImportParams.length cannot be 0', - }); - const cryptoKey = hmac.toCryptoKey({ name: algorithm, hash }, extractable, usages); - assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, length); - } + const key = secretKey(length); + for (const hash of hashes) { + vectors.push(vector( + key, + { name: 'HMAC', hash }, + ['sign', 'verify'], + { length })); } } + return vectors; } -{ - const algorithms = ['Ed25519', 'X25519']; +function kmacVectors(name) { + return [ + vector( + secretKey(256), + { name }, + ['sign', 'verify'], + { length: 256 }), + ]; +} - if (!process.features.openssl_is_boringssl) { - algorithms.push('X448', 'Ed448'); - } else { - common.printSkipMessage('Skipping unsupported Ed448/X448 test cases'); +function asymmetricVectors(keyPair, algorithm, usagesByType, expected) { + const { publicKey, privateKey } = keyPair; + const vectors = []; + for (const keyObject of [publicKey, privateKey]) { + vectors.push(vector( + keyObject, + algorithm, + getUsages(keyObject, usagesByType), + expected)); } + return vectors; +} - for (const algorithm of algorithms) { - const { publicKey, privateKey } = generateKeyPairSync(algorithm.toLowerCase()); - assert.throws(() => { - publicKey.toCryptoKey(algorithm === 'Ed25519' ? 'X25519' : 'Ed25519', true, []); - }, { - name: 'DataError', - message: 'Invalid key type' - }); - for (const key of [publicKey, privateKey]) { - let usages; - if (algorithm.startsWith('E')) { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } else { - usages = key.type === 'public' ? [] : ['deriveBits']; - } - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey(algorithm, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - } +function rsaVectors(name, usagesByType) { + const keyPair = generateKeyPairSync('rsa', { modulusLength: 2048 }); + const vectors = []; + for (const keyObject of [keyPair.publicKey, keyPair.privateKey]) { + for (const hash of hashes) { + vectors.push(vector( + keyObject, + { name, hash }, + getUsages(keyObject, usagesByType), + { hash })); } } + return vectors; } -{ - const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); - for (const key of [publicKey, privateKey]) { - for (const algorithm of ['RSASSA-PKCS1-v1_5', 'RSA-PSS', 'RSA-OAEP']) { - let usages; - if (algorithm === 'RSA-OAEP') { - usages = key.type === 'public' ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; - } else { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } - for (const extractable of [true, false]) { - for (const hash of ['SHA-1', 'SHA-256', 'SHA-384', 'SHA-512']) { - const cryptoKey = key.toCryptoKey({ - name: algorithm, - hash - }, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.hash.name, hash); - } - } +function ecVectors(name, usagesByType) { + const vectors = []; + for (const namedCurve of namedCurves) { + const keyPair = generateKeyPairSync('ec', { namedCurve }); + for (const keyObject of [keyPair.publicKey, keyPair.privateKey]) { + vectors.push(vector( + keyObject, + { name, namedCurve }, + getUsages(keyObject, usagesByType), + { namedCurve })); } } + return vectors; } -{ - for (const namedCurve of ['P-256', 'P-384', 'P-521']) { - const { publicKey, privateKey } = generateKeyPairSync('ec', { namedCurve }); - assert.throws(() => { - privateKey.toCryptoKey({ - name: 'ECDH', - namedCurve, - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a private key.' - }); - assert.throws(() => { - publicKey.toCryptoKey({ - name: 'ECDH', - namedCurve: namedCurve === 'P-256' ? 'P-384' : 'P-256' - }, true, []); - }, { - name: 'DataError', - message: 'Named curve mismatch' - }); - for (const key of [publicKey, privateKey]) { - for (const algorithm of ['ECDH', 'ECDSA']) { - let usages; - if (algorithm === 'ECDH') { - usages = key.type === 'public' ? [] : ['deriveBits']; - } else { - usages = key.type === 'public' ? ['verify'] : ['sign']; - } - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey({ - name: algorithm, - namedCurve - }, extractable, usages); - assertCryptoKey(cryptoKey, key, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.namedCurve, namedCurve); - } - } - } +function cfrgVectors(name, usagesByType) { + const keyPair = generateKeyPairSync(name.toLowerCase()); + return asymmetricVectors(keyPair, name, usagesByType); +} + +function simpleAsymmetricVectors(name, usagesByType) { + const keyPair = generateKeyPairSync(name.toLowerCase()); + return asymmetricVectors(keyPair, { name }, usagesByType); +} + +function genericSecretInvalid(name) { + const key = createSecretKey(randomBytes(16)); + assert.throws(() => key.toCryptoKey(name, true, ['deriveBits']), { + name: 'SyntaxError', + message: `${name} keys are not extractable` + }); + assert.throws(() => key.toCryptoKey(name, false, ['wrapKey']), { + name: 'SyntaxError', + message: `Unsupported key usage for a ${name} key` + }); +} + +function ecdhInvalid() { + const { publicKey, privateKey } = generateKeyPairSync('ec', { + namedCurve: 'P-256', + }); + + assert.throws(() => { + privateKey.toCryptoKey({ + name: 'ECDH', + namedCurve: 'P-256', + }, true, []); + }, { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a private key.' + }); + + assert.throws(() => { + publicKey.toCryptoKey({ + name: 'ECDH', + namedCurve: 'P-384', + }, true, []); + }, { + name: 'DataError', + message: 'Named curve mismatch' + }); +} + +function invalidAsymmetricKeyType(name, invalidAlgorithm) { + const { publicKey } = generateKeyPairSync(name.toLowerCase()); + assert.throws(() => { + publicKey.toCryptoKey(invalidAlgorithm, true, []); + }, { + name: 'DataError', + message: 'Invalid key type' + }); +} + +function emptyPrivateUsagesInvalid(name) { + const { privateKey } = generateKeyPairSync(name.toLowerCase()); + assert.throws(() => { + privateKey.toCryptoKey(name, true, []); + }, { + name: 'SyntaxError', + message: 'Usages cannot be empty when importing a private key.' + }); +} + +const tests = { + 'AES-KW': symmetricVectors('AES-KW', ['wrapKey', 'unwrapKey']), + 'ECDH': ecVectors('ECDH', derivationUsages), + 'ECDSA': ecVectors('ECDSA', signingUsages), + 'Ed25519': cfrgVectors('Ed25519', signingUsages), + 'HMAC': hmacVectors(), + 'RSA-OAEP': rsaVectors('RSA-OAEP', rsaOaepUsages), + 'RSA-PSS': rsaVectors('RSA-PSS', signingUsages), + 'RSASSA-PKCS1-v1_5': rsaVectors('RSASSA-PKCS1-v1_5', signingUsages), + 'X25519': cfrgVectors('X25519', derivationUsages), +}; + +const invalid = { + 'ECDH': ecdhInvalid, + 'Ed25519': () => invalidAsymmetricKeyType('Ed25519', 'X25519'), + 'HMAC': () => macInvalid( + { name: 'HMAC', hash: 'SHA-256' }, + 'HmacImportParams.length cannot be 0'), + 'X25519': () => invalidAsymmetricKeyType('X25519', 'Ed25519'), +}; + +for (const name of ['AES-CBC', 'AES-CTR', 'AES-GCM', 'AES-OCB']) { + if (name in kSupportedAlgorithms.importKey) + tests[name] = symmetricVectors(name, ['encrypt', 'decrypt']); +} + +if ('ChaCha20-Poly1305' in kSupportedAlgorithms.importKey) + tests['ChaCha20-Poly1305'] = chacha20Poly1305Vectors(); + +for (const name of ['HKDF', 'PBKDF2', 'Argon2d', 'Argon2i', 'Argon2id']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = genericSecretVectors(name); + invalid[name] = () => genericSecretInvalid(name); } } -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(() => { - privateKey.toCryptoKey(name, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a private key.' - }); - for (const key of [publicKey, privateKey]) { - const usages = key.type === 'public' ? ['verify'] : ['sign']; - for (const extractable of [true, false]) { - const cryptoKey = key.toCryptoKey({ name }, extractable, usages); - assertCryptoKey(cryptoKey, key, name, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.name, name); - } - } +for (const name of ['KMAC128', 'KMAC256']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = kmacVectors(name); + invalid[name] = () => { + macInvalid({ name }, 'KmacImportParams.length cannot be 0'); + }; + } +} + +for (const [name, usages, invalidAlgorithm] of [ + ['Ed448', signingUsages, 'X25519'], + ['X448', derivationUsages, 'Ed25519'], +]) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = cfrgVectors(name, usages); + invalid[name] = () => { + invalidAsymmetricKeyType(name, invalidAlgorithm); + }; } } -if (hasOpenSSL(3)) { - for (const algorithm of ['KMAC128', 'KMAC256']) { - const hmac = createSecretKey(randomBytes(32)); - const usages = ['sign', 'verify']; - - assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey({ name: algorithm }, true, usages); - }, { - name: 'DataError', - message: 'Zero-length key is not supported', - }); - - assert.throws(() => { - hmac.toCryptoKey({ - name: algorithm, - }, true, []); - }, { - name: 'SyntaxError', - message: 'Usages cannot be empty when importing a secret key.' - }); - - for (const extractable of [true, false]) { - assert.throws(() => { - hmac.toCryptoKey({ name: algorithm, length: 0 }, true, usages); - }, { - name: 'DataError', - message: 'KmacImportParams.length cannot be 0', - }); - const cryptoKey = hmac.toCryptoKey({ name: algorithm }, extractable, usages); - assertCryptoKey(cryptoKey, hmac, algorithm, extractable, usages); - assert.strictEqual(cryptoKey.algorithm.length, 256); +for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = simpleAsymmetricVectors(name, signingUsages); + invalid[name] = () => emptyPrivateUsagesInvalid(name); + } +} + +for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { + if (name in kSupportedAlgorithms.importKey) { + tests[name] = simpleAsymmetricVectors(name, mlKemUsages); + invalid[name] = () => emptyPrivateUsagesInvalid(name); + } +} + +const unsupportedToCryptoKeyAlgorithms = new Set(); + +for (const name of Object.keys(kSupportedAlgorithms.importKey)) { + const vectors = tests[name]; + if (vectors === undefined) { + if (!unsupportedToCryptoKeyAlgorithms.has(name)) { + assert.fail( + `${name} needs a tests entry or must be listed in ` + + 'unsupportedToCryptoKeyAlgorithms'); } + continue; } + + for (const vector of vectors) + runVector(vector); + + invalid[name]?.(); } diff --git a/test/parallel/test-webcrypto-get-public-key.mjs b/test/parallel/test-webcrypto-get-public-key.mjs index 9764aabd0a887e..622ec4adca6891 100644 --- a/test/parallel/test-webcrypto-get-public-key.mjs +++ b/test/parallel/test-webcrypto-get-public-key.mjs @@ -1,8 +1,15 @@ +// Flags: --expose-internals + import * as common from '../common/index.mjs'; if (!common.hasCrypto) common.skip('missing crypto'); import * as assert from 'node:assert'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const { kSupportedAlgorithms } = require('internal/crypto/util'); +const { SubtleCrypto } = globalThis; const { subtle } = globalThis.crypto; const RSA_KEY_GEN = { @@ -11,27 +18,71 @@ const RSA_KEY_GEN = { hash: 'SHA-256', }; -const publicUsages = { - 'ECDH': [], - 'ECDSA': ['verify'], - 'Ed25519': ['verify'], - 'RSA-OAEP': ['encrypt', 'wrapKey'], - 'RSA-PSS': ['verify'], - 'RSASSA-PKCS1-v1_5': ['verify'], - 'X25519': [], +function vector(algorithm, privateUsages, publicUsages) { + return { algorithm, privateUsages, publicUsages }; +} + +const keyGeneration = { + 'ECDH': vector({ name: 'ECDH', namedCurve: 'P-256' }, ['deriveBits'], []), + 'ECDSA': vector({ name: 'ECDSA', namedCurve: 'P-256' }, ['sign'], ['verify']), + 'RSA-OAEP': vector( + { name: 'RSA-OAEP', ...RSA_KEY_GEN }, + ['decrypt', 'unwrapKey'], + ['encrypt', 'wrapKey']), + 'RSA-PSS': vector({ name: 'RSA-PSS', ...RSA_KEY_GEN }, ['sign'], ['verify']), + 'RSASSA-PKCS1-v1_5': vector( + { name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, + ['sign'], + ['verify']), }; -for await (const { privateKey } of [ - subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']), - subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']), - subtle.generateKey('Ed25519', false, ['sign']), - subtle.generateKey({ name: 'RSA-OAEP', ...RSA_KEY_GEN }, false, ['decrypt', 'unwrapKey']), - subtle.generateKey({ name: 'RSA-PSS', ...RSA_KEY_GEN }, false, ['sign']), - subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, false, ['sign']), - subtle.generateKey('X25519', false, ['deriveBits']), +for (const name of [ + 'Ed25519', + 'Ed448', + 'ML-DSA-44', + 'ML-DSA-65', + 'ML-DSA-87', ]) { - const { name } = privateKey.algorithm; - const usages = publicUsages[name]; + keyGeneration[name] = vector(name, ['sign'], ['verify']); +} + +for (const name of ['X25519', 'X448']) { + keyGeneration[name] = vector(name, ['deriveBits'], []); +} + +for (const name of ['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']) { + keyGeneration[name] = vector(name, ['decapsulateBits'], ['encapsulateBits']); +} + +const unsupportedGetPublicKeyAlgorithms = new Set([ + 'AES-CBC', + 'AES-CTR', + 'AES-GCM', + 'AES-KW', + 'AES-OCB', + 'ChaCha20-Poly1305', + 'HMAC', + 'KMAC128', + 'KMAC256', +]); + +for (const name of Object.keys(kSupportedAlgorithms.exportKey)) { + const test = keyGeneration[name]; + if (test === undefined) { + if (!unsupportedGetPublicKeyAlgorithms.has(name)) { + assert.fail( + `${name} needs a keyGeneration entry or must be listed in ` + + 'unsupportedGetPublicKeyAlgorithms'); + } + assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), false); + continue; + } + + assert.strictEqual(SubtleCrypto.supports('getPublicKey', name), true); + + const { privateKey } = await subtle.generateKey( + test.algorithm, false, test.privateUsages); + const usages = test.publicUsages; const publicKey = await subtle.getPublicKey(privateKey, usages); assert.deepStrictEqual(publicKey.algorithm, privateKey.algorithm); assert.strictEqual(publicKey.type, 'public'); @@ -50,7 +101,9 @@ for await (const { privateKey } of [ const secretKey = await subtle.generateKey( { name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); -await assert.rejects(() => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), { - name: 'NotSupportedError', - message: 'key must be a private key' -}); +await assert.rejects( + () => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), + { + name: 'NotSupportedError', + message: 'key must be a private key' + }); From 532597c2622bc0e382ebd348023138bfd5570c17 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Mon, 15 Jun 2026 21:34:10 -0400 Subject: [PATCH 5/9] test: update WPT for WebCryptoAPI to 03a1476844 PR-URL: https://github.com/nodejs/node/pull/63900 Reviewed-By: Filip Skokan Reviewed-By: Antoine du Hamel Reviewed-By: Colin Ihrig Reviewed-By: Luigi Pinca --- test/fixtures/wpt/README.md | 4 +- .../digest/cshake.tentative.https.any.js | 10 +- .../getPublicKey.tentative.https.any.js | 36 ++++ .../import_export/symmetric_importKey.js | 43 ++++ .../wpt/WebCryptoAPI/sign_verify/kmac.js | 36 ++-- .../WebCryptoAPI/sign_verify/kmac_vectors.js | 12 +- .../supports-modern.tentative.https.any.js | 160 +++++++++++++++ .../supports.tentative.https.any.js | 191 +++++++++++++++++- test/fixtures/wpt/versions.json | 2 +- test/wpt/status/WebCryptoAPI.cjs | 61 +----- 10 files changed, 457 insertions(+), 98 deletions(-) create mode 100644 test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 67be1d3780932c..1d4324a93b6872 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -29,12 +29,12 @@ Last update: - 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/d4598eba09/url -- urlpattern: https://github.com/web-platform-tests/wpt/tree/23aac92784/urlpattern +- urlpattern: https://github.com/web-platform-tests/wpt/tree/11a459a2b1/urlpattern - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - 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/97bbc7247a/WebCryptoAPI +- WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/03a1476844/WebCryptoAPI - webidl: https://github.com/web-platform-tests/wpt/tree/63ca529a02/webidl - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/2f96fa1996/webidl/ecmascript-binding/es-exceptions - webmessaging/broadcastchannel: https://github.com/web-platform-tests/wpt/tree/6495c91853/webmessaging/broadcastchannel diff --git a/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js index 71c717b62550bc..81793666294c23 100644 --- a/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/digest/cshake.tentative.https.any.js @@ -163,7 +163,7 @@ Object.keys(digestedData).forEach(function (alg) { Object.keys(sourceData).forEach(function (size) { promise_test(function (test) { return crypto.subtle - .digest({ name: alg, length: length }, sourceData[size]) + .digest({ name: alg, outputLength: length }, sourceData[size]) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), @@ -184,7 +184,7 @@ Object.keys(digestedData).forEach(function (alg) { buffer[0] = sourceData[size][0]; return alg; }, - length + outputLength: length }, buffer) .then(function (result) { assert_true( @@ -197,7 +197,7 @@ Object.keys(digestedData).forEach(function (alg) { promise_test(function (test) { var buffer = new Uint8Array(sourceData[size]); var promise = crypto.subtle - .digest({ name: alg, length: length }, buffer) + .digest({ name: alg, outputLength: length }, buffer) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), @@ -218,7 +218,7 @@ Object.keys(digestedData).forEach(function (alg) { buffer.buffer.transfer(); return alg; }, - length + outputLength: length }, buffer) .then(function (result) { assert_true( @@ -231,7 +231,7 @@ Object.keys(digestedData).forEach(function (alg) { promise_test(function (test) { var buffer = new Uint8Array(sourceData[size]); var promise = crypto.subtle - .digest({ name: alg, length: length }, buffer) + .digest({ name: alg, outputLength: length }, buffer) .then(function (result) { assert_true( equalBuffers(result, digestedData[alg][length][size]), diff --git a/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js index 1b151345c3416d..73e4ad8231b04c 100644 --- a/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/getPublicKey.tentative.https.any.js @@ -61,6 +61,42 @@ const algorithms = [ generateKeyParams: { name: "X25519" }, usages: ["deriveKey", "deriveBits"], publicKeyUsages: [] + }, + { + name: "ML-DSA-44", + generateKeyParams: { name: "ML-DSA-44" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-DSA-65", + generateKeyParams: { name: "ML-DSA-65" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-DSA-87", + generateKeyParams: { name: "ML-DSA-87" }, + usages: ["sign", "verify"], + publicKeyUsages: ["verify"] + }, + { + name: "ML-KEM-512", + generateKeyParams: { name: "ML-KEM-512" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] + }, + { + name: "ML-KEM-768", + generateKeyParams: { name: "ML-KEM-768" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] + }, + { + name: "ML-KEM-1024", + generateKeyParams: { name: "ML-KEM-1024" }, + usages: ["encapsulateBits", "encapsulateKey", "decapsulateBits", "decapsulateKey"], + publicKeyUsages: ["encapsulateBits", "encapsulateKey"] } ]; diff --git a/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js b/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js index 8633af5279caa1..795d90ffbff4fd 100644 --- a/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js +++ b/test/fixtures/wpt/WebCryptoAPI/import_export/symmetric_importKey.js @@ -59,6 +59,20 @@ function runTests(algorithmName) { testFormat(format, algorithm, data, keyData.length * 8, usages, extractable); }); testEmptyUsages(format, algorithm, data, keyData.length * 8, extractable); + + // Verify the key data is copied only after the algorithm is + // normalized (per spec, "get a copy of the bytes held by the + // keyData parameter" follows "normalize an algorithm"). A + // getter on the algorithm name mutates the key data during + // normalization; the imported key must reflect the restored + // bytes. Checked for BufferSource formats (jwk key data is + // not a BufferSource) when the key is extractable, so the + // imported bytes can be read back and compared. + if (extractable && format !== "jwk") { + allValidUsages(vector.legalUsages).forEach(function(usages) { + testKeyDataAlteredDuringCall(format, algorithm, keyData, keyData.length * 8, usages); + }); + } }); }); @@ -111,6 +125,35 @@ function runTests(algorithmName) { }, "Empty Usages: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, extractable, usages)); } + // Test that importKey copies the key data only after the algorithm has been + // normalized. The spec normalizes the algorithm (which can run author + // getters on the algorithm dictionary) before getting a copy of the bytes + // held by the keyData parameter. Here a getter on the algorithm's name + // mutates the key data during normalization and then restores it, so a + // spec-compliant implementation imports the restored (original) bytes. + function testKeyDataAlteredDuringCall(format, algorithm, keyData, keySize, usages) { + promise_test(function(test) { + var alteredKeyData = copyBuffer(keyData); + alteredKeyData[0] = 255 - alteredKeyData[0]; + var algorithmWithGetter = { + ...algorithm, + get name() { + alteredKeyData[0] = keyData[0]; + return algorithm.name; + } + }; + return subtle.importKey(format, alteredKeyData, algorithmWithGetter, true, usages). + then(function(key) { + return subtle.exportKey(format, key); + }). + then(function(result) { + assert_true(equalBuffers(keyData, result), "Imported key reflects the key data as it was after normalization"); + }, function(err) { + assert_unreached("Threw an unexpected error: " + err.toString()); + }); + }, "Key data altered during call: " + keySize.toString() + " bits " + parameterString(format, keyData, algorithm, true, usages)); + } + // Helper methods follow: diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js index c696fa46c064a4..d9d82d150b3933 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac.js @@ -15,7 +15,7 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -48,7 +48,7 @@ function run_test() { var signature = copyBuffer(vector.signature); signature[0] = 255 - signature[0]; var algorithmParams = { - length: vector.length, + outputLength: vector.outputLength, get name() { signature[0] = vector.signature[0]; return vector.algorithm; @@ -81,7 +81,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var signature = copyBuffer(vector.signature); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -115,7 +115,7 @@ function run_test() { signature.buffer.transfer(); return vector.algorithm; }, - length: vector.length + outputLength: vector.outputLength }; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; @@ -144,7 +144,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var signature = copyBuffer(vector.signature); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -175,7 +175,7 @@ function run_test() { var plaintext = copyBuffer(vector.plaintext); plaintext[0] = 255 - plaintext[0]; var algorithmParams = { - length: vector.length, + outputLength: vector.outputLength, get name() { plaintext[0] = vector.plaintext[0]; return vector.algorithm; @@ -208,7 +208,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var plaintext = copyBuffer(vector.plaintext); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -242,7 +242,7 @@ function run_test() { plaintext.buffer.transfer(); return vector.algorithm; }, - length: vector.length + outputLength: vector.outputLength }; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; @@ -271,7 +271,7 @@ function run_test() { .then(function(vector) { promise_test(function(test) { var plaintext = copyBuffer(vector.plaintext); - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -301,7 +301,7 @@ function run_test() { var promise = importVectorKeys(vector, ["sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -326,7 +326,7 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vectors) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -363,7 +363,7 @@ function run_test() { return importVectorKeys(vector, ["verify", "sign"]) .then(function(vectors) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -401,7 +401,7 @@ function run_test() { return importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -438,7 +438,7 @@ function run_test() { var plaintext = copyBuffer(vector.plaintext); plaintext[0] = 255 - plaintext[0]; promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -470,7 +470,7 @@ function run_test() { var signature = copyBuffer(vector.signature); signature[0] = 255 - signature[0]; promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -501,7 +501,7 @@ function run_test() { .then(function(vector) { var signature = vector.signature.slice(1); // Drop first byte promise_test(function(test) { - var algorithmParams = {name: vector.algorithm, length: vector.length}; + var algorithmParams = {name: vector.algorithm, outputLength: vector.outputLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } @@ -531,8 +531,8 @@ function run_test() { var promise = importVectorKeys(vector, ["verify", "sign"]) .then(function(vector) { promise_test(function(test) { - var differentLength = vector.length === 256 ? 512 : 256; - var algorithmParams = {name: vector.algorithm, length: differentLength}; + var differentLength = vector.outputLength === 256 ? 512 : 256; + var algorithmParams = {name: vector.algorithm, outputLength: differentLength}; if (vector.customization !== undefined) { algorithmParams.customization = vector.customization; } diff --git a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js index 39d0efe71ba3e2..6b35cab168c9c6 100644 --- a/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js +++ b/test/fixtures/wpt/WebCryptoAPI/sign_verify/kmac_vectors.js @@ -6,7 +6,7 @@ function getTestVectors() { // Sample #1 - KMAC128, no customization name: "KMAC128 with no customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -25,7 +25,7 @@ function getTestVectors() { // Sample #2 - KMAC128, with customization name: "KMAC128 with customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -47,7 +47,7 @@ function getTestVectors() { // Sample #3 - KMAC128, large data, with customization name: "KMAC128 with large data and customization", algorithm: "KMAC128", - length: 256, + outputLength: 256, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -69,7 +69,7 @@ function getTestVectors() { // Sample #4 - KMAC256, with customization, 512-bit output name: "KMAC256 with customization and 512-bit output", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -94,7 +94,7 @@ function getTestVectors() { // Sample #5 - KMAC256, large data, no customization, 512-bit output name: "KMAC256 with large data and no customization", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, @@ -116,7 +116,7 @@ function getTestVectors() { // Sample #6 - KMAC256, large data, with customization, 512-bit output name: "KMAC256 with large data and customization", algorithm: "KMAC256", - length: 512, + outputLength: 512, keyBuffer: new Uint8Array([ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, diff --git a/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js new file mode 100644 index 00000000000000..ca23b782317762 --- /dev/null +++ b/test/fixtures/wpt/WebCryptoAPI/supports-modern.tentative.https.any.js @@ -0,0 +1,160 @@ +// META: title=WebCrypto API: supports method tests for algorithms in https://wicg.github.io/webcrypto-modern-algos/ +// META: script=util/helpers.js + +'use strict'; + +const modernAlgorithms = { + // Asymmetric algorithms + 'ML-DSA-44': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-DSA-65': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-DSA-87': { + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], + }, + 'ML-KEM-512': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + 'ML-KEM-768': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + 'ML-KEM-1024': { + operations: [ + 'generateKey', 'importKey', 'encapsulateKey', 'encapsulateBits', + 'decapsulateKey', 'decapsulateBits', 'getPublicKey' + ], + }, + + // Symmetric algorithms + 'ChaCha20-Poly1305': { + operations: ['generateKey', 'importKey', 'encrypt', 'decrypt'], + encryptParams: {name: 'ChaCha20-Poly1305', iv: new Uint8Array(12)}, + }, + +}; + +const operations = [ + 'generateKey', + 'importKey', + 'sign', + 'verify', + 'encrypt', + 'decrypt', + 'deriveBits', + 'digest', + 'encapsulateKey', + 'encapsulateBits', + 'decapsulateKey', + 'decapsulateBits', + 'getPublicKey', +]; + +// Test that supports method exists and is a static method +test(() => { + assert_true( + typeof SubtleCrypto.supports === 'function', + 'SubtleCrypto.supports should be a function'); +}, 'SubtleCrypto.supports method exists'); + + +// Test standard WebCrypto algorithms for requested operations +for (const [algorithmName, algorithmInfo] of Object.entries(modernAlgorithms)) { + for (const operation of operations) { + promise_test(async (t) => { + const isSupported = algorithmInfo.operations.includes(operation); + + // Use appropriate algorithm parameters for each operation + let algorithm; + let lengthOrAdditionalAlgorithm; + switch (operation) { + case 'generateKey': + algorithm = algorithmInfo.keyGenParams || algorithmName; + break; + case 'importKey': + algorithm = algorithmInfo.importParams || algorithmName; + break; + case 'sign': + case 'verify': + algorithm = algorithmInfo.signParams || algorithmName; + break; + case 'encrypt': + case 'decrypt': + algorithm = algorithmInfo.encryptParams || algorithmName; + break; + case 'deriveBits': + algorithm = algorithmInfo.deriveBitsParams || algorithmName; + if (algorithm?.public instanceof Promise) { + algorithm.public = (await algorithm.public).publicKey; + } + if (algorithmName === 'PBKDF2' || algorithmName === 'HKDF') { + lengthOrAdditionalAlgorithm = 256; + } + break; + case 'digest': + algorithm = algorithmName; + break; + case 'encapsulateKey': + case 'encapsulateBits': + case 'decapsulateKey': + case 'decapsulateBits': + algorithm = algorithmName; + if (operation === 'encapsulateKey' || operation === 'decapsulateKey') { + lengthOrAdditionalAlgorithm = { name: 'AES-GCM', length: 256 }; + } + break; + default: + algorithm = algorithmName; + } + + const result = SubtleCrypto.supports(operation, algorithm, lengthOrAdditionalAlgorithm); + + if (isSupported) { + assert_true(result, `${algorithmName} should support ${operation}`); + } else { + assert_false( + result, `${algorithmName} should not support ${operation}`); + } + }, `supports(${operation}, ${algorithmName})`); + } +} + +// Test some algorithm objects with valid parameters +test(() => { + assert_true( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with valid tagLength'); +}, 'supports returns true for algorithm objects with valid parameters'); + +// Test some algorithm objects with invalid parameters +test(() => { + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(12), + tagLength: 100, + }), + 'ChaCha20-Poly1305 encrypt with invalid tagLength'); + + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'ChaCha20-Poly1305', + iv: new Uint8Array(10), + tagLength: 128, + }), + 'ChaCha20-Poly1305 encrypt with invalid iv'); +}, 'supports returns false for algorithm objects with invalid parameters'); + + +done(); diff --git a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js index dd39273e4b918b..ac3a32c741000c 100644 --- a/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js +++ b/test/fixtures/wpt/WebCryptoAPI/supports.tentative.https.any.js @@ -6,7 +6,7 @@ const standardAlgorithms = { // Asymmetric algorithms 'RSASSA-PKCS1-v1_5': { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, @@ -17,7 +17,7 @@ const standardAlgorithms = { signParams: { name: 'RSASSA-PKCS1-v1_5' }, }, 'RSA-PSS': { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'RSA-PSS', modulusLength: 2048, @@ -28,7 +28,7 @@ const standardAlgorithms = { signParams: { name: 'RSA-PSS', saltLength: 32 }, }, 'RSA-OAEP': { - operations: ['generateKey', 'importKey', 'encrypt', 'decrypt'], + operations: ['generateKey', 'importKey', 'encrypt', 'decrypt', 'getPublicKey'], keyGenParams: { name: 'RSA-OAEP', modulusLength: 2048, @@ -39,13 +39,13 @@ const standardAlgorithms = { encryptParams: { name: 'RSA-OAEP' }, }, ECDSA: { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: { name: 'ECDSA', namedCurve: 'P-256' }, importParams: { name: 'ECDSA', namedCurve: 'P-256' }, signParams: { name: 'ECDSA', hash: 'SHA-256' }, }, ECDH: { - operations: ['generateKey', 'importKey', 'deriveBits'], + operations: ['generateKey', 'importKey', 'deriveBits', 'getPublicKey'], keyGenParams: { name: 'ECDH', namedCurve: 'P-256' }, importParams: { name: 'ECDH', namedCurve: 'P-256' }, deriveBitsParams: { @@ -58,12 +58,12 @@ const standardAlgorithms = { }, }, Ed25519: { - operations: ['generateKey', 'importKey', 'sign', 'verify'], + operations: ['generateKey', 'importKey', 'sign', 'verify', 'getPublicKey'], keyGenParams: null, signParams: { name: 'Ed25519' }, }, X25519: { - operations: ['generateKey', 'importKey', 'deriveBits'], + operations: ['generateKey', 'importKey', 'deriveBits', 'getPublicKey'], keyGenParams: null, deriveBitsParams: { name: 'X25519', @@ -152,6 +152,7 @@ const operations = [ 'decrypt', 'deriveBits', 'digest', + 'getPublicKey', ]; // Test that supports method exists and is a static method @@ -268,6 +269,137 @@ test(() => { }), 'Invalid hash parameter should return false' ); + assert_false( + SubtleCrypto.supports( + 'encrypt', {name: 'AES-CBC', iv: new Uint8Array(10)}), + 'Invalid IV for AES-CBC should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(10), + length: 128, + }), + 'Invalid IV for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 0, + }), + 'Invalid length=0 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-CTR', + counter: new Uint8Array(16), + length: 129, + }), + 'Invalid length=129 for AES-CTR should return false'); + assert_false( + SubtleCrypto.supports('encrypt', { + name: 'AES-GCM', + iv: new Uint8Array(16), + tagLength: 100, + }), + 'Invalid tag length for AES-GCM should return false'); + assert_false( + SubtleCrypto.supports('generateKey', {name: 'ECDH', namedCurve: 'P-51'}), + 'Invalid curve for ECDH should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-25', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 8), + 'Invalid hash for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + 11), + 'Invalid length for HKDF should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }), + 'null length for HKDF should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-25', + }), + 'Invalid hash for HMAC should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'HMAC', + hash: 'SHA-256', + length: 0, + }), + 'Invalid length for HMAC should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-25', + salt: new Uint8Array(16), + iterations: 100000, + }, + 8), + 'Invalid hash for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }, + 11), + 'Invalid length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports( + 'deriveBits', { + name: 'PBKDF2', + hash: 'SHA-256', + salt: new Uint8Array(16), + iterations: 100000, + }), + 'null length for PBKDF2 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PKCS1 should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-PSS', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-56', + }), + 'Invalid hash for RSA PSS should return false'); + assert_false( + SubtleCrypto.supports('generateKey', { + name: 'RSA-OAEP', + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + hash: 'SHA-26', + }), + 'Invalid hash for RSA OAEP should return false'); + }, 'supports returns false for algorithm objects with invalid parameters'); // Test some specific combinations that should work @@ -394,4 +526,49 @@ test(() => { ); }, 'Invalid algorithm and operation combinations fail'); +// Test supports for deriveKey op +test(() => { + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'HKDF', + hash: 'SHA-256', + salt: new Uint8Array(16), + info: new Uint8Array(0), + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey HKDF-HMAC should pass'); +}, 'deriveKey tests'); + +promise_test(async (t) => { + let keypair = await crypto.subtle.generateKey( + { + name: 'X25519', + }, + false, ['deriveKey', 'deriveBits']); + + assert_true( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'AES-GCM', length: 256}), + + 'deriveKey X25519-AES-GCM-256 should pass'); + + assert_false( + SubtleCrypto.supports( + 'deriveKey', { + name: 'X25519', + public: keypair.publicKey, + }, + {name: 'HMAC', hash: 'SHA-256'}), + + 'deriveKey X25519-HMAC-SHA-256 should fail'); +}, 'deriveKey promise tests'); + + + done(); diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 440400cfe03767..bb3bb4d731d98d 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -96,7 +96,7 @@ "path": "web-locks" }, "WebCryptoAPI": { - "commit": "97bbc7247a16231f4744a47a1d9b3d29633d5292", + "commit": "03a14768441c1ce2a31678d0dc080e81dc449d0a", "path": "WebCryptoAPI" }, "webidl": { diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index 5d1d630c436311..c9e32046c3cf69 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -62,6 +62,7 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) { 'sign_verify/mldsa.tentative.https.any.js'); skipSubtests( + ['getPublicKey.tentative.https.any.js', /ml-(?:kem|dsa)/i], ['supports-modern.tentative.https.any.js', /ml-(?:kem|dsa)/i]); } @@ -88,6 +89,7 @@ if (process.features.openssl_is_boringssl) { ['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], + ['getPublicKey.tentative.https.any.js', /(?:ed448|x448|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]); @@ -104,53 +106,6 @@ function assertNoOverlap(fileSkips, subtestSkips) { assertNoOverlap(conditionalFileSkips, conditionalSubtestSkips); -const cshakeExpectedFailures = ['cSHAKE128', 'cSHAKE256'].flatMap((algorithm) => { - return [0, 256, 384, 512].flatMap((length) => { - return ['empty', 'short', 'medium'].flatMap((size) => { - const base = `${algorithm} with ${length} bit output and ${size} source data`; - return [ - base, - ].concat(size !== 'empty' ? [ - `${base} and altered buffer after call`, - `${base} and altered buffer during call`, - `${base} and transferred buffer after call`, - `${base} and transferred buffer during call`, - ] : []); - }); - }); -}); - -const kmacVectorNames = [ - 'KMAC128 with no customization', - 'KMAC128 with customization', - 'KMAC128 with large data and customization', - 'KMAC256 with customization and 512-bit output', - 'KMAC256 with large data and no customization', - 'KMAC256 with large data and customization', -]; - -const kmacExpectedFailures = kmacVectorNames.flatMap((name) => { - return [ - `${name} verification`, - `${name} verification with transferred signature during call`, - `${name} verification with transferred signature after call`, - `${name} verification with altered signature during call`, - `${name} verification with altered signature after call`, - `${name} with altered plaintext during call`, - `${name} with altered plaintext after call`, - `${name} with transferred plaintext during call`, - `${name} with transferred plaintext after call`, - `${name} no verify usage`, - `${name} round trip`, - `${name} verification failure due to wrong plaintext`, - `${name} verification failure due to wrong signature`, - `${name} verification failure due to short signature`, - `${name} verification failure due to wrong length parameter`, - `${name} signing with wrong algorithm name`, - `${name} verifying with wrong algorithm name`, - ]; -}); - module.exports = { ...conditionalFileSkips, ...conditionalSubtestSkips, @@ -191,16 +146,4 @@ module.exports = { ], }, }, - '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': conditionalFileSkips['sign_verify/kmac.tentative.https.any.js'] ?? { - 'fail': { - 'note': 'WPT still uses KmacParams.length; implementation moved to KmacParams.outputLength', - 'expected': kmacExpectedFailures, - }, - }, }; From 5c68b466221af456f185c734affc52356452d0d2 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 20 Jun 2026 15:29:46 +0200 Subject: [PATCH 6/9] crypto: share WebCrypto method and usage helpers Introduce prepareSubtleMethod() and convertSubtleArgument() for the common SubtleCrypto call prelude. Methods now reuse the same receiver check, required-argument check, error prefix construction, argument context selection, and WebIDL conversion path. Introduce WebCrypto key usage helpers for common usage validation and key pair usage splitting. Algorithm modules now define their allowed public, private, and key generation usages once, then call the shared helpers from generateKey() and importKey() paths. This removes repeated function-invocation setup in webcrypto.js and repeated key usage checks across the WebCrypto algorithm modules while preserving the existing validation behavior. Mark normalized-algorithm fallback branches as unreachable assertions. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63975 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- lib/internal/crypto/aes.js | 48 +- lib/internal/crypto/cfrg.js | 114 ++--- lib/internal/crypto/chacha20_poly1305.js | 33 +- lib/internal/crypto/ec.js | 100 ++-- lib/internal/crypto/hash.js | 9 +- lib/internal/crypto/mac.js | 40 +- lib/internal/crypto/ml_dsa.js | 61 +-- lib/internal/crypto/ml_kem.js | 67 ++- lib/internal/crypto/rsa.js | 106 ++-- lib/internal/crypto/util.js | 17 - lib/internal/crypto/webcrypto.js | 600 ++++++++--------------- lib/internal/crypto/webcrypto_util.js | 118 ++++- 12 files changed, 541 insertions(+), 772 deletions(-) diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index dd7c5ac5fd247a..7ce1dabbf7c5b6 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -1,10 +1,5 @@ 'use strict'; -const { - ArrayPrototypePush, - SafeSet, -} = primordials; - const { AESCipherJob, kCryptoJobWebCrypto, @@ -28,7 +23,6 @@ const { const { getUsagesMask, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -46,8 +40,22 @@ const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kCipherUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; +const kWrapUsages = ['wrapKey', 'unwrapKey']; + +const kUsages = { + '__proto__': null, + 'AES-CBC': kCipherUsages, + 'AES-CTR': kCipherUsages, + 'AES-GCM': kCipherUsages, + 'AES-KW': kWrapUsages, + 'AES-OCB': kCipherUsages, +}; + function getAlgorithmName(name, length) { switch (name) { case 'AES-CBC': return `A${length}CBC`; @@ -176,21 +184,8 @@ function aesCipher(mode, key, data, algorithm) { function aesGenerateKey(algorithm, extractable, usages) { const { name, length } = algorithm; - const checkUsages = ['wrapKey', 'unwrapKey']; - if (name !== 'AES-KW') - ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - 'Unsupported key usage for an AES key', - 'SyntaxError'); - } - if (usagesSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usagesSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages[name], name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -207,16 +202,7 @@ function aesImportKey( extractable, usages) { const { name } = algorithm; - const checkUsages = ['wrapKey', 'unwrapKey']; - if (name !== 'AES-KW') - ArrayPrototypePush(checkUsages, 'encrypt', 'decrypt'); - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - 'Unsupported key usage for an AES key', - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages[name], name); let handle; let length; diff --git a/lib/internal/crypto/cfrg.js b/lib/internal/crypto/cfrg.js index 1099466eec3ebb..b561ae479abffd 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -28,8 +28,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -44,60 +42,33 @@ const { } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableCfrgKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'X25519': - // Fall through - case 'X448': - checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; - break; - case 'Ed25519': - // Fall through - case 'Ed448': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kDeriveUsages = createKeyUsages([], ['deriveKey', 'deriveBits']); + +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); + +const kUsages = { + '__proto__': null, + 'X25519': kDeriveUsages, + 'X448': kDeriveUsages, + 'Ed25519': kSignVerifyUsages, + 'Ed448': kSignVerifyUsages, +}; function cfrgGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - switch (name) { - case 'Ed25519': - // Fall through - case 'Ed448': - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } - break; - case 'X25519': - // Fall through - case 'X448': - if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } - break; - } + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const nid = { '__proto__': null, 'Ed25519': EVP_PKEY_ED25519, @@ -106,37 +77,16 @@ function cfrgGenerateKey(algorithm, extractable, usages) { 'X448': EVP_PKEY_X448, }[name]; - let publicUsages; - let privateUsages; - switch (name) { - case 'Ed25519': - // Fall through - case 'Ed448': - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - case 'X25519': - // Fall through - case 'X448': - publicUsages = new SafeSet(); - privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); - break; - } - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -175,20 +125,25 @@ function cfrgImportKey( const { name } = algorithm; let handle; + const allowedUsages = kUsages[name]; const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObjectHandle': - verifyAcceptableCfrgKeyUse( - name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); handle = keyData; break; case 'spki': { - verifyAcceptableCfrgKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableCfrgKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -207,7 +162,10 @@ function cfrgImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableCfrgKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); if (!isPublic) { @@ -218,7 +176,7 @@ function cfrgImportKey( break; } case 'raw': { - verifyAcceptableCfrgKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importRawKey(true, keyData, kKeyFormatRawPublic, name); break; } diff --git a/lib/internal/crypto/chacha20_poly1305.js b/lib/internal/crypto/chacha20_poly1305.js index 8baaf6f981df21..45071cded217ee 100644 --- a/lib/internal/crypto/chacha20_poly1305.js +++ b/lib/internal/crypto/chacha20_poly1305.js @@ -1,9 +1,5 @@ 'use strict'; -const { - SafeSet, -} = primordials; - const { ChaCha20Poly1305CipherJob, SecretKeyGenJob, @@ -12,7 +8,6 @@ const { const { getUsagesMask, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -29,8 +24,12 @@ const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; + function validateKeyLength(length) { if (length !== 256) throw lazyDOMException('Invalid key length', 'DataError'); @@ -49,19 +48,8 @@ function c20pCipher(mode, key, data, algorithm) { function c20pGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - `Unsupported key usage for a ${algorithm.name} key`, - 'SyntaxError'); - } - if (usagesSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usagesSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -78,14 +66,9 @@ function c20pImportKey( extractable, usages) { const { name } = algorithm; - const checkUsages = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']; - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, checkUsages)) { - throw lazyDOMException( - `Unsupported key usage for a ${algorithm.name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages( + usages, kUsages, name); let handle; switch (format) { diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js index 94040cc7f71813..fc19e7cce6a5e7 100644 --- a/lib/internal/crypto/ec.js +++ b/lib/internal/crypto/ec.js @@ -30,8 +30,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, normalizeHashName, kNamedCurveAliases, @@ -49,81 +47,43 @@ const { } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableEcKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'ECDH': - checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; - break; - case 'ECDSA': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kDeriveUsages = createKeyUsages([], ['deriveKey', 'deriveBits']); -function ecGenerateKey(algorithm, extractable, usages) { - const { name, namedCurve } = algorithm; +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); - const usageSet = new SafeSet(usages); - switch (name) { - case 'ECDSA': - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for an ECDSA key', - 'SyntaxError'); - } - break; - case 'ECDH': - if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) { - throw lazyDOMException( - 'Unsupported key usage for an ECDH key', - 'SyntaxError'); - } - // Fall through - } +const kUsages = { + '__proto__': null, + 'ECDH': kDeriveUsages, + 'ECDSA': kSignVerifyUsages, +}; - let publicUsages; - let privateUsages; - switch (name) { - case 'ECDSA': - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - case 'ECDH': - publicUsages = new SafeSet(); - privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); - break; - } +function ecGenerateKey(algorithm, extractable, usages) { + const { name, namedCurve } = algorithm; + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const keyAlgorithm = { name, namedCurve }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new EcKeyPairGenJob( kCryptoJobWebCrypto, namedCurve, undefined, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -181,20 +141,25 @@ function ecImportKey( const { name, namedCurve } = algorithm; let handle; + const allowedUsages = kUsages[name]; const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObjectHandle': - verifyAcceptableEcKeyUse( - name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); handle = keyData; break; case 'spki': { - verifyAcceptableEcKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableEcKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -221,12 +186,15 @@ function ecImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableEcKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } case 'raw': { - verifyAcceptableEcKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, allowedUsages.public); handle = importRawKey(true, keyData, kKeyFormatRawPublic, 'ec', namedCurve); break; } diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index f8bb7f66b8cb60..58d309f4d86d68 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -33,7 +33,6 @@ const { } = require('internal/crypto/keys'); const { - lazyDOMException, normalizeEncoding, encodingsMap, isPendingDeprecation, @@ -250,9 +249,13 @@ function asyncDigest(algorithm, data) { algorithm.customization, algorithm.outputLength / 8, data)); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function hash(algorithm, input, options) { diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 5c6955d22b1904..7562231688c7e8 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -1,7 +1,6 @@ 'use strict'; const { - SafeSet, StringPrototypeSubstring, } = primordials; @@ -17,7 +16,6 @@ const { const { getBlockSize, getUsagesMask, - hasAnyNotIn, jobPromise, normalizeHashName, } = require('internal/crypto/util'); @@ -36,8 +34,12 @@ const { importJwkSecretKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, } = require('internal/crypto/webcrypto_util'); +const kUsages = ['sign', 'verify']; + function hmacGenerateKey(algorithm, extractable, usages) { const { hash, @@ -45,17 +47,8 @@ function hmacGenerateKey(algorithm, extractable, usages) { length = getBlockSize(hash.name), } = algorithm; - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for an HMAC key', - 'SyntaxError'); - } - if (usageSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usageSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -75,17 +68,8 @@ function kmacGenerateKey(algorithm, extractable, usages) { }[name], } = algorithm; - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for ${name} key`, - 'SyntaxError'); - } - if (usageSet.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const usageSet = validateUsagesNotEmpty( + validateKeyUsages(usages, kUsages, name)); return jobPromise(() => new SecretKeyGenJob( kCryptoJobWebCrypto, @@ -103,12 +87,8 @@ function macImportKey( usages, ) { const isHmac = algorithm.name === 'HMAC'; - const usagesSet = new SafeSet(usages); - if (hasAnyNotIn(usagesSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for ${algorithm.name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages( + usages, kUsages, algorithm.name); let handle; let length; switch (format) { diff --git a/lib/internal/crypto/ml_dsa.js b/lib/internal/crypto/ml_dsa.js index 196976223a8d3b..bfaa57d92c46d1 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -30,8 +30,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -47,30 +45,22 @@ const { } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableMlDsaKeyUse(name, isPublic, usages) { - const checkSet = isPublic ? ['verify'] : ['sign']; - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} +const kUsages = createKeyUsages(['verify'], ['sign']); function mlDsaGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages.keygen, name); const nid = { '__proto__': null, @@ -79,23 +69,16 @@ function mlDsaGenerateKey(algorithm, extractable, usages) { 'ML-DSA-87': EVP_PKEY_ML_DSA_87, }[name]; - const publicUsages = getUsagesUnion(usageSet, 'verify'); - const privateUsages = getUsagesUnion(usageSet, 'sign'); - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, kUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -150,17 +133,21 @@ function mlDsaImportKey( const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObjectHandle': - verifyAcceptableMlDsaKeyUse( - name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + kUsages.public : + kUsages.private); handle = keyData; break; case 'spki': { - verifyAcceptableMlDsaKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableMlDsaKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.private); const privOnlyLengths = { '__proto__': null, @@ -185,7 +172,10 @@ function mlDsaImportKey( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); const isPublic = keyData.priv === undefined; - verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); if (!isPublic) { @@ -198,7 +188,10 @@ function mlDsaImportKey( case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; - verifyAcceptableMlDsaKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } diff --git a/lib/internal/crypto/ml_kem.js b/lib/internal/crypto/ml_kem.js index ef3a45c56aaeee..bc3caf917c78ac 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -27,8 +27,6 @@ const { const { getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, } = require('internal/crypto/util'); @@ -44,21 +42,24 @@ const { } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importRawKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); +const kUsages = createKeyUsages( + ['encapsulateKey', 'encapsulateBits'], + ['decapsulateKey', 'decapsulateBits']); + function mlKemGenerateKey(algorithm, extractable, usages) { const { name } = algorithm; - - const usageSet = new SafeSet(usages); - if (hasAnyNotIn(usageSet, ['encapsulateKey', 'encapsulateBits', 'decapsulateKey', 'decapsulateBits'])) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } + const usagesSet = validateKeyUsages(usages, kUsages.keygen, name); const nid = { '__proto__': null, @@ -67,25 +68,16 @@ function mlKemGenerateKey(algorithm, extractable, usages) { 'ML-KEM-1024': EVP_PKEY_ML_KEM_1024, }[name]; - const publicUsages = - getUsagesUnion(usageSet, 'encapsulateKey', 'encapsulateBits'); - const privateUsages = - getUsagesUnion(usageSet, 'decapsulateKey', 'decapsulateBits'); - const keyAlgorithm = { name }; - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, kUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new NidKeyPairGenJob( kCryptoJobWebCrypto, nid, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -128,15 +120,6 @@ function mlKemExportKey(key, format) { } } -function verifyAcceptableMlKemKeyUse(name, isPublic, usages) { - const checkSet = isPublic ? ['encapsulateKey', 'encapsulateBits'] : ['decapsulateKey', 'decapsulateBits']; - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for a ${name} key`, - 'SyntaxError'); - } -} - function mlKemImportKey( format, keyData, @@ -149,17 +132,21 @@ function mlKemImportKey( const usagesSet = new SafeSet(usages); switch (format) { case 'KeyObjectHandle': - verifyAcceptableMlKemKeyUse( - name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + kUsages.public : + kUsages.private); handle = keyData; break; case 'spki': { - verifyAcceptableMlKemKeyUse(name, true, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableMlKemKeyUse(name, false, usagesSet); + verifyAcceptableKeyUse(name, usagesSet, kUsages.private); const privOnlyLengths = { '__proto__': null, @@ -179,7 +166,10 @@ function mlKemImportKey( case 'raw-public': case 'raw-seed': { const isPublic = format === 'raw-public'; - verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importRawKey(isPublic, keyData, isPublic ? kKeyFormatRawPublic : kKeyFormatRawSeed, name); break; } @@ -191,7 +181,10 @@ function mlKemImportKey( 'JWK "alg" Parameter and algorithm name mismatch', 'DataError'); const isPublic = keyData.priv === undefined; - verifyAcceptableMlKemKeyUse(name, isPublic, usagesSet); + verifyAcceptableKeyUse( + name, + usagesSet, + isPublic ? kUsages.public : kUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/rsa.js b/lib/internal/crypto/rsa.js index 0442f8a80db0d4..a2757384f4f490 100644 --- a/lib/internal/crypto/rsa.js +++ b/lib/internal/crypto/rsa.js @@ -32,8 +32,6 @@ const { bigIntArrayToUnsignedInt, getDigestSizeInBytes, getUsagesMask, - getUsagesUnion, - hasAnyNotIn, jobPromise, normalizeHashName, validateMaxBufferLength, @@ -51,32 +49,28 @@ const { } = require('internal/crypto/keys'); const { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, } = require('internal/crypto/webcrypto_util'); -function verifyAcceptableRsaKeyUse(name, isPublic, usages) { - let checkSet; - switch (name) { - case 'RSA-OAEP': - checkSet = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']; - break; - case 'RSA-PSS': - // Fall through - case 'RSASSA-PKCS1-v1_5': - checkSet = isPublic ? ['verify'] : ['sign']; - break; - default: - throw lazyDOMException( - 'The algorithm is not supported', 'NotSupportedError'); - } - if (hasAnyNotIn(usages, checkSet)) { - throw lazyDOMException( - `Unsupported key usage for an ${name} key`, - 'SyntaxError'); - } -} +const kOaepUsages = createKeyUsages( + ['encrypt', 'wrapKey'], + ['decrypt', 'unwrapKey']); + +const kSignVerifyUsages = createKeyUsages(['verify'], ['sign']); + +const kUsages = { + '__proto__': null, + 'RSA-OAEP': kOaepUsages, + 'RSA-PSS': kSignVerifyUsages, + 'RSASSA-PKCS1-v1_5': kSignVerifyUsages, +}; function validateRsaOaepAlgorithm(algorithm) { if (algorithm.label !== undefined) { @@ -122,24 +116,8 @@ function rsaKeyGenerate( hash, } = algorithm; - const usageSet = new SafeSet(usages); - - switch (name) { - case 'RSA-OAEP': - if (hasAnyNotIn(usageSet, - ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'])) { - throw lazyDOMException( - 'Unsupported key usage for a RSA key', - 'SyntaxError'); - } - break; - default: - if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { - throw lazyDOMException( - 'Unsupported key usage for a RSA key', - 'SyntaxError'); - } - } + const allowedUsages = kUsages[name]; + const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name); const keyAlgorithm = { name, @@ -154,26 +132,8 @@ function rsaKeyGenerate( 'OperationError'); } - let publicUsages; - let privateUsages; - switch (name) { - case 'RSA-OAEP': { - publicUsages = getUsagesUnion(usageSet, 'encrypt', 'wrapKey'); - privateUsages = getUsagesUnion(usageSet, 'decrypt', 'unwrapKey'); - break; - } - default: { - publicUsages = getUsagesUnion(usageSet, 'verify'); - privateUsages = getUsagesUnion(usageSet, 'sign'); - break; - } - } - - if (privateUsages.size === 0) { - throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError'); - } + const keyUsages = getKeyPairUsages(usagesSet, allowedUsages); + validateUsagesNotEmpty(keyUsages.private); return jobPromise(() => new RsaKeyPairGenJob( kCryptoJobWebCrypto, @@ -181,8 +141,8 @@ function rsaKeyGenerate( modulusLength, publicExponentConverted, keyAlgorithm, - getUsagesMask(publicUsages), - getUsagesMask(privateUsages), + getUsagesMask(keyUsages.public), + getUsagesMask(keyUsages.private), extractable)); } @@ -213,21 +173,28 @@ function rsaImportKey( algorithm, extractable, usages) { + const allowedUsages = kUsages[algorithm.name]; const usagesSet = new SafeSet(usages); let handle; switch (format) { case 'KeyObjectHandle': - verifyAcceptableRsaKeyUse( - algorithm.name, keyData.getKeyType() === kKeyTypePublic, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, + usagesSet, + keyData.getKeyType() === kKeyTypePublic ? + allowedUsages.public : + allowedUsages.private); handle = keyData; break; case 'spki': { - verifyAcceptableRsaKeyUse(algorithm.name, true, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, usagesSet, allowedUsages.public); handle = importDerKey(keyData, true); break; } case 'pkcs8': { - verifyAcceptableRsaKeyUse(algorithm.name, false, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, usagesSet, allowedUsages.private); handle = importDerKey(keyData, false); break; } @@ -249,7 +216,10 @@ function rsaImportKey( } const isPublic = keyData.d === undefined; - verifyAcceptableRsaKeyUse(algorithm.name, isPublic, usagesSet); + verifyAcceptableKeyUse( + algorithm.name, + usagesSet, + isPublic ? allowedUsages.public : allowedUsages.private); handle = importJwkKey(isPublic, keyData); break; } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index ddb053967389ed..cd55ad9e4b2901 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -18,7 +18,6 @@ const { PromiseReject, PromiseWithResolvers, SafeMap, - SafeSet, StringPrototypeToUpperCase, Symbol, TypedArrayPrototypeGetBuffer, @@ -830,21 +829,6 @@ 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++) { - if (usageSet.has(usages[n])) - newset.add(usages[n]); - } - return newset; -} - // Must be at most 31 entries. const kCanonicalUsageOrder = [ 'encrypt', 'decrypt', @@ -1024,7 +1008,6 @@ module.exports = { getBlockSize, getDigestSizeInBytes, getStringOption, - getUsagesUnion, getUsagesMask, getUsagesFromMask, hasUsage, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index f40557367951e0..aa62c66a78502f 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -109,24 +109,42 @@ function callSubtleCryptoMethod(fn, receiver, args) { } } +const kArgumentContexts = [ + '1st argument', + '2nd argument', + '3rd argument', + '4th argument', + '5th argument', + '6th argument', + '7th argument', +]; + +function prepareSubtleMethod(receiver, method, argLength, required) { + if (receiver !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); + + webidl ??= require('internal/crypto/webidl'); + const prefix = `Failed to execute '${method}' on 'SubtleCrypto'`; + webidl.requiredArguments(argLength, required, { prefix }); + return prefix; +} + +function convertSubtleArgument(prefix, converter, value, index) { + return webidl.converters[converter](value, { + prefix, + context: kArgumentContexts[index], + }); +} + 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'); - const prefix = "Failed to execute 'digest' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'digest', arguments.length, 2); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'digest'); @@ -149,23 +167,13 @@ function generateKeyImpl( algorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'generateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '2nd argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'generateKey', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'generateKey'); switch (normalizedAlgorithm.name) { @@ -226,8 +234,12 @@ function generateKeyImpl( case 'KMAC256': return require('internal/crypto/mac') .kmacGenerateKey(normalizedAlgorithm, extractable, usages); - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } } @@ -236,24 +248,13 @@ 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'); - const prefix = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - baseKey = webidl.converters.CryptoKey(baseKey, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'deriveBits', arguments.length, 2); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + baseKey = convertSubtleArgument(prefix, 'CryptoKey', baseKey, i++); if (length !== null) { - length = webidl.converters['unsigned long'](length, { - prefix, - context: '3rd argument', - }); + length = convertSubtleArgument(prefix, 'unsigned long', length, i++); } const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); @@ -285,8 +286,13 @@ function deriveBitsImpl(algorithm, baseKey, length = null) { case 'Argon2id': return require('internal/crypto/argon2') .argon2DeriveBits(normalizedAlgorithm, baseKey, length); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function getKeyLength({ name, length, hash }) { @@ -343,31 +349,16 @@ function deriveKeyImpl( derivedKeyType, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - baseKey = webidl.converters.CryptoKey(baseKey, { - prefix, - context: '2nd argument', - }); - derivedKeyType = webidl.converters.AlgorithmIdentifier(derivedKeyType, { - prefix, - context: '3rd argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + const prefix = prepareSubtleMethod(this, 'deriveKey', arguments.length, 5); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + baseKey = convertSubtleArgument(prefix, 'CryptoKey', baseKey, i++); + derivedKeyType = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', derivedKeyType, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits'); const normalizedDerivedKeyAlgorithmImport = @@ -409,8 +400,12 @@ function deriveKeyImpl( secret = require('internal/crypto/argon2') .argon2DeriveBits(normalizedAlgorithm, baseKey, length); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(secret, (secret) => FunctionPrototypeCall( @@ -767,19 +762,10 @@ function exportKey(format, key) { } 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', - }); + const prefix = prepareSubtleMethod(this, 'exportKey', arguments.length, 2); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); return exportKeySync(format, key); } @@ -1009,32 +995,16 @@ function importKeyImpl( algorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'importKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); + const prefix = prepareSubtleMethod(this, 'importKey', arguments.length, 5); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); const type = format === 'jwk' ? 'JsonWebKey' : 'BufferSource'; - keyData = webidl.converters[type](keyData, { - prefix, - context: '2nd argument', - }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '3rd argument', - }); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + keyData = convertSubtleArgument(prefix, type, keyData, i++); + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey'); @@ -1056,29 +1026,14 @@ function wrapKey(format, key, wrappingKey, wrapAlgorithm) { } function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'wrapKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 4, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - wrappingKey = webidl.converters.CryptoKey(wrappingKey, { - prefix, - context: '3rd argument', - }); - const algorithm = webidl.converters.AlgorithmIdentifier(wrapAlgorithm, { - prefix, - context: '4th argument', - }); + const prefix = prepareSubtleMethod(this, 'wrapKey', arguments.length, 4); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + wrappingKey = convertSubtleArgument(prefix, 'CryptoKey', wrappingKey, i++); + const algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', wrapAlgorithm, i++); let normalizedAlgorithm; - try { normalizedAlgorithm = normalizeAlgorithm(algorithm, 'wrapKey'); } catch { @@ -1118,8 +1073,7 @@ function wrapKeyImpl(format, key, wrappingKey, wrapAlgorithm) { kWebCryptoCipherEncrypt, normalizedAlgorithm, wrappingKey, - bytes, - 'wrapKey'); + bytes); } // subtle.unwrapKey() is essentially a subtle.decrypt() followed @@ -1143,42 +1097,19 @@ function unwrapKeyImpl( unwrappedKeyAlgorithm, extractable, keyUsages) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'unwrapKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 7, { prefix }); - format = webidl.converters.KeyFormat(format, { - prefix, - context: '1st argument', - }); - wrappedKey = webidl.converters.BufferSource(wrappedKey, { - prefix, - context: '2nd argument', - }); - unwrappingKey = webidl.converters.CryptoKey(unwrappingKey, { - prefix, - context: '3rd argument', - }); - const algorithm = webidl.converters.AlgorithmIdentifier(unwrapAlgorithm, { - prefix, - context: '4th argument', - }); - unwrappedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - unwrappedKeyAlgorithm, - { - prefix, - context: '5th argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '6th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '7th argument', - }); + const prefix = prepareSubtleMethod(this, 'unwrapKey', arguments.length, 7); + let i = 0; + format = convertSubtleArgument(prefix, 'KeyFormat', format, i++); + wrappedKey = convertSubtleArgument(prefix, 'BufferSource', wrappedKey, i++); + unwrappingKey = convertSubtleArgument( + prefix, 'CryptoKey', unwrappingKey, i++); + const algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', unwrapAlgorithm, i++); + unwrappedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', unwrappedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); let normalizedAlgorithm; try { @@ -1201,8 +1132,7 @@ function unwrapKeyImpl( kWebCryptoCipherDecrypt, normalizedAlgorithm, unwrappingKey, - wrappedKey, - 'unwrapKey'); + wrappedKey); return jobPromiseThen(bytes, (bytes) => { let keyData = bytes; @@ -1263,8 +1193,13 @@ function signVerify(algorithm, key, data, signature) { case 'KMAC256': return require('internal/crypto/mac') .kmacSignVerify(key, data, normalizedAlgorithm, signature); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function sign(algorithm, key, data) { @@ -1272,23 +1207,12 @@ function sign(algorithm, key, data) { } function signImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'sign' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'sign', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); return signVerify(algorithm, key, data); } @@ -1298,32 +1222,18 @@ 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'); - const prefix = "Failed to execute 'verify' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 4, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - signature = webidl.converters.BufferSource(signature, { - prefix, - context: '3rd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '4th argument', - }); + const prefix = prepareSubtleMethod(this, 'verify', arguments.length, 4); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + signature = convertSubtleArgument(prefix, 'BufferSource', signature, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); return signVerify(algorithm, key, data, signature); } -function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { +function cipherOrWrap(mode, normalizedAlgorithm, key, data) { // 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. @@ -1340,18 +1250,20 @@ function cipherOrWrap(mode, normalizedAlgorithm, key, data, operation) { case 'AES-GCM': // Fall through case 'AES-OCB': + // Fall through + case 'AES-KW': return require('internal/crypto/aes') .aesCipher(mode, key, data, normalizedAlgorithm); case 'ChaCha20-Poly1305': return require('internal/crypto/chacha20_poly1305') .c20pCipher(mode, key, data, normalizedAlgorithm); - case 'AES-KW': - if (operation === 'wrapKey' || operation === 'unwrapKey') { - return require('internal/crypto/aes') - .aesCipher(mode, key, data, normalizedAlgorithm); - } + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function encrypt(algorithm, key, data) { @@ -1359,23 +1271,12 @@ function encrypt(algorithm, key, data) { } function encryptImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encrypt' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'encrypt', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt'); @@ -1391,7 +1292,6 @@ function encryptImpl(algorithm, key, data) { normalizedAlgorithm, key, data, - 'encrypt', ); } @@ -1400,23 +1300,12 @@ function decrypt(algorithm, key, data) { } function decryptImpl(algorithm, key, data) { - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decrypt' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - algorithm = webidl.converters.AlgorithmIdentifier(algorithm, { - prefix, - context: '1st argument', - }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '2nd argument', - }); - data = webidl.converters.BufferSource(data, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod(this, 'decrypt', arguments.length, 3); + let i = 0; + algorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', algorithm, i++); + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + data = convertSubtleArgument(prefix, 'BufferSource', data, i++); const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'decrypt'); @@ -1432,7 +1321,6 @@ function decryptImpl(algorithm, key, data) { normalizedAlgorithm, key, data, - 'decrypt', ); } @@ -1443,19 +1331,11 @@ function getPublicKey(key, keyUsages) { function getPublicKeyImpl(key, keyUsages) { emitExperimentalWarning('The getPublicKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'getPublicKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - key = webidl.converters.CryptoKey(key, { - prefix, - context: '1st argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod(this, 'getPublicKey', arguments.length, 2); + let i = 0; + key = convertSubtleArgument(prefix, 'CryptoKey', key, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const type = getCryptoKeyType(key); if (type !== 'private') @@ -1471,22 +1351,13 @@ function encapsulateBits(encapsulationAlgorithm, encapsulationKey) { function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { emitExperimentalWarning('The encapsulateBits Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encapsulateBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - encapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { - prefix, - context: '2nd argument', - }); + const prefix = prepareSubtleMethod( + this, 'encapsulateBits', arguments.length, 2); + let i = 0; + encapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', encapsulationAlgorithm, i++); + encapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', encapsulationKey, i++); const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); @@ -1510,9 +1381,13 @@ function encapsulateBitsImpl(encapsulationAlgorithm, encapsulationKey) { case 'ML-KEM-1024': return require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function encapsulateKey( @@ -1531,37 +1406,18 @@ function encapsulateKeyImpl( extractable, keyUsages) { emitExperimentalWarning('The encapsulateKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'encapsulateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 5, { prefix }); - encapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - encapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - encapsulationKey = webidl.converters.CryptoKey(encapsulationKey, { - prefix, - context: '2nd argument', - }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - sharedKeyAlgorithm, - { - prefix, - context: '3rd argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '4th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '5th argument', - }); + const prefix = prepareSubtleMethod( + this, 'encapsulateKey', arguments.length, 5); + let i = 0; + encapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', encapsulationAlgorithm, i++); + encapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', encapsulationKey, i++); + sharedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', sharedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedEncapsulationAlgorithm = normalizeAlgorithm(encapsulationAlgorithm, 'encapsulate'); @@ -1589,8 +1445,12 @@ function encapsulateKeyImpl( encapsulatedBits = require('internal/crypto/ml_kem') .mlKemEncapsulate(encapsulationKey); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(encapsulatedBits, (encapsulatedBits) => { @@ -1617,26 +1477,14 @@ 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'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decapsulateBits' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 3, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - decapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { - prefix, - context: '2nd argument', - }); - ciphertext = webidl.converters.BufferSource(ciphertext, { - prefix, - context: '3rd argument', - }); + const prefix = prepareSubtleMethod( + this, 'decapsulateBits', arguments.length, 3); + let i = 0; + decapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', decapsulationAlgorithm, i++); + decapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', decapsulationKey, i++); + ciphertext = convertSubtleArgument(prefix, 'BufferSource', ciphertext, i++); const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); @@ -1660,9 +1508,13 @@ function decapsulateBitsImpl(decapsulationAlgorithm, decapsulationKey, ciphertex case 'ML-KEM-1024': return require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } - - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } function decapsulateKey( @@ -1683,41 +1535,19 @@ function decapsulateKeyImpl( extractable, keyUsages) { emitExperimentalWarning('The decapsulateKey Web Crypto API method'); - if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); - - webidl ??= require('internal/crypto/webidl'); - const prefix = "Failed to execute 'decapsulateKey' on 'SubtleCrypto'"; - webidl.requiredArguments(arguments.length, 6, { prefix }); - decapsulationAlgorithm = webidl.converters.AlgorithmIdentifier( - decapsulationAlgorithm, - { - prefix, - context: '1st argument', - }, - ); - decapsulationKey = webidl.converters.CryptoKey(decapsulationKey, { - prefix, - context: '2nd argument', - }); - ciphertext = webidl.converters.BufferSource(ciphertext, { - prefix, - context: '3rd argument', - }); - sharedKeyAlgorithm = webidl.converters.AlgorithmIdentifier( - sharedKeyAlgorithm, - { - prefix, - context: '4th argument', - }, - ); - extractable = webidl.converters.boolean(extractable, { - prefix, - context: '5th argument', - }); - const usages = webidl.converters['sequence'](keyUsages, { - prefix, - context: '6th argument', - }); + const prefix = prepareSubtleMethod( + this, 'decapsulateKey', arguments.length, 6); + let i = 0; + decapsulationAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', decapsulationAlgorithm, i++); + decapsulationKey = convertSubtleArgument( + prefix, 'CryptoKey', decapsulationKey, i++); + ciphertext = convertSubtleArgument(prefix, 'BufferSource', ciphertext, i++); + sharedKeyAlgorithm = convertSubtleArgument( + prefix, 'AlgorithmIdentifier', sharedKeyAlgorithm, i++); + extractable = convertSubtleArgument(prefix, 'boolean', extractable, i++); + const usages = convertSubtleArgument( + prefix, 'sequence', keyUsages, i++); const normalizedDecapsulationAlgorithm = normalizeAlgorithm(decapsulationAlgorithm, 'decapsulate'); @@ -1745,8 +1575,12 @@ function decapsulateKeyImpl( decapsulatedBits = require('internal/crypto/ml_kem') .mlKemDecapsulate(decapsulationKey, ciphertext); break; - default: - throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); + /* c8 ignore start */ + default: { + const assert = require('internal/assert'); + assert.fail('Unreachable code'); + } + /* c8 ignore stop */ } return jobPromiseThen(decapsulatedBits, (decapsulatedBits) => FunctionPrototypeCall( @@ -2017,10 +1851,12 @@ function check(op, alg, length) { return true; } + /* c8 ignore start */ default: { const assert = require('internal/assert'); assert.fail('Unreachable code'); } + /* c8 ignore stop */ } } diff --git a/lib/internal/crypto/webcrypto_util.js b/lib/internal/crypto/webcrypto_util.js index 320bf0436de553..1b802a6dc46624 100644 --- a/lib/internal/crypto/webcrypto_util.js +++ b/lib/internal/crypto/webcrypto_util.js @@ -1,5 +1,10 @@ 'use strict'; +const { + ArrayPrototypePush, + SafeSet, +} = primordials; + const { KeyObjectHandle, kKeyFormatDER, @@ -12,6 +17,7 @@ const { } = internalBinding('crypto'); const { + hasAnyNotIn, validateKeyOps, } = require('internal/crypto/util'); @@ -19,6 +25,110 @@ const { lazyDOMException, } = require('internal/util'); +/** + * @typedef {object} KeyUsageLists + * @property {string[]} public Usages accepted for public keys. + * @property {string[]} private Usages accepted for private keys. + * @property {string[]} keygen Usages accepted during key generation. + */ + +/** + * @typedef {object} KeyUsageSets + * @property {SafeSet} public Requested public key usages. + * @property {SafeSet} private Requested private key usages. + */ + +/** + * Validates that every requested key usage is allowed for `subject`. + * @param {string} subject + * @param {SafeSet} usagesSet + * @param {string[]} allowed + */ +function verifyAcceptableKeyUse(subject, usagesSet, allowed) { + if (hasAnyNotIn(usagesSet, allowed)) { + throw lazyDOMException( + `Unsupported key usage for ${subject} key`, + 'SyntaxError'); + } +} + +/** + * Converts a usage list to a set and validates it against `allowed`. + * @param {string[]} usages + * @param {string[]} allowed + * @param {string} subject + * @returns {SafeSet} + */ +function validateKeyUsages(usages, allowed, subject) { + const usagesSet = new SafeSet(usages); + verifyAcceptableKeyUse(subject, usagesSet, allowed); + return usagesSet; +} + +/** + * Validates that a usage set is not empty. + * @param {SafeSet} usagesSet + * @returns {SafeSet} + */ +function validateUsagesNotEmpty(usagesSet) { + if (usagesSet.size === 0) { + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError'); + } + return usagesSet; +} + +/** + * Returns the requested usages that are present in `usagesSet`. + * @param {SafeSet} usagesSet + * @param {string[]} usages + * @returns {SafeSet} + */ +function getUsagesUnion(usagesSet, usages) { + const newset = new SafeSet(); + for (let n = 0; n < usages.length; n++) { + if (usagesSet.has(usages[n])) + newset.add(usages[n]); + } + return newset; +} + +/** + * Splits requested usages into public and private key usage sets. + * @param {SafeSet} usagesSet + * @param {KeyUsageLists} allowed + * @returns {KeyUsageSets} + */ +function getKeyPairUsages(usagesSet, allowed) { + return { + public: getUsagesUnion(usagesSet, allowed.public), + private: getUsagesUnion(usagesSet, allowed.private), + }; +} + +/** + * Creates the accepted public, private, and key generation usage lists. + * @param {string[]} publicUsages + * @param {string[]} privateUsages + * @returns {KeyUsageLists} + */ +function createKeyUsages(publicUsages, privateUsages) { + const keygen = []; + for (let n = 0; n < publicUsages.length; n++) { + ArrayPrototypePush(keygen, publicUsages[n]); + } + for (let n = 0; n < privateUsages.length; n++) { + ArrayPrototypePush(keygen, privateUsages[n]); + } + return { + __proto__: null, + public: publicUsages, + private: privateUsages, + keygen, + }; +} + function importDerKey(keyData, isPublic) { const handle = new KeyObjectHandle(); const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate; @@ -74,11 +184,12 @@ function validateJwk(keyData, kty, extractable, usagesSet, expectedUse) { (keyData.priv !== undefined && typeof keyData.priv !== 'string')) throw lazyDOMException('Invalid keyData', 'DataError'); break; + /* c8 ignore start */ default: { - // It is not possible to get here because all possible cases are handled above. const assert = require('internal/assert'); assert.fail('Unreachable code'); } + /* c8 ignore stop */ } if (usagesSet.size > 0 && keyData.use !== undefined) { if (keyData.use !== expectedUse) @@ -136,10 +247,15 @@ function importJwkSecretKey(keyData) { } module.exports = { + createKeyUsages, + getKeyPairUsages, importDerKey, importJwkKey, importJwkSecretKey, importRawKey, importSecretKey, validateJwk, + validateKeyUsages, + validateUsagesNotEmpty, + verifyAcceptableKeyUse, }; From 5ef6d8446acb1455f874d44add474a3144b63cb7 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 18 Jun 2026 18:12:10 +0200 Subject: [PATCH 7/9] crypto: support non-byte WebCrypto lengths and cSHAKE Add shared bit-length helpers for WebCrypto operations that accept bit sequences whose length is not byte-aligned. Use the helpers for cSHAKE output, ECDH-derived bits, HMAC/KMAC key generation/import/derivation, and KMAC sign/verify output. Preserve the requested bit length in CryptoKey algorithm metadata while storing and exporting rounded-up byte material with unused low bits cleared. Keep byte-multiple validation for algorithms whose specs require it. Extend the lower-end of KMAC's key length support. Enable cSHAKE customization and functionName parameters. Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/63988 Reviewed-By: James M Snell Reviewed-By: Yagiz Nizipli --- doc/api/webcrypto.md | 31 +- lib/internal/crypto/diffiehellman.js | 26 +- lib/internal/crypto/hash.js | 39 +- lib/internal/crypto/mac.js | 37 +- lib/internal/crypto/util.js | 60 ++- lib/internal/crypto/webcrypto.js | 14 +- lib/internal/crypto/webidl.js | 83 ++-- src/crypto/crypto_hash.cc | 363 +++++++++++++++++- src/crypto/crypto_hash.h | 68 ++++ src/crypto/crypto_keygen.cc | 10 +- src/crypto/crypto_keygen.h | 6 +- src/crypto/crypto_kmac.cc | 100 ++++- src/crypto/crypto_kmac.h | 3 +- src/crypto/crypto_util.cc | 24 ++ src/crypto/crypto_util.h | 9 + test/fixtures/crypto/kmac.js | 55 +++ test/fixtures/webcrypto/supports-level-2.mjs | 10 +- .../webcrypto/supports-modern-algorithms.mjs | 40 +- test/fixtures/webcrypto/supports-sha3.mjs | 8 +- .../test-crypto-key-objects-to-crypto-key.js | 26 +- .../parallel/test-webcrypto-derivekey-ecdh.js | 19 + test/parallel/test-webcrypto-derivekey.js | 19 +- test/parallel/test-webcrypto-digest.js | 151 +++++++- .../test-webcrypto-encap-decap-ml-kem.js | 32 ++ test/parallel/test-webcrypto-export-import.js | 116 +++++- test/parallel/test-webcrypto-keygen-kmac.js | 7 +- .../test-webcrypto-sign-verify-kmac.js | 88 ++++- test/parallel/test-webcrypto-supports.mjs | 8 +- test/parallel/test-webcrypto-wrap-unwrap.js | 83 ++++ 29 files changed, 1389 insertions(+), 146 deletions(-) diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index fd4b5609a42f2f..898d7779d783d2 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -1924,26 +1924,39 @@ added: v24.15.0 * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `functionName` member represents the function name, used by NIST to define -functions based on cSHAKE. -The Node.js Web Crypto API implementation only supports zero-length functionName -which is equivalent to not providing functionName at all. +The `functionName` member represents the NIST function-name byte string used to +domain-separate functions built on top of cSHAKE. Accepted values are: + +* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* the ASCII byte sequence `'KMAC'` +* the ASCII byte sequence `'TupleHash'` +* the ASCII byte sequence `'ParallelHash'` #### `cShakeParams.customization` * Type: {ArrayBuffer|TypedArray|DataView|Buffer|undefined} -The `customization` member represents the customization string. -The Node.js Web Crypto API implementation only supports zero-length customization -which is equivalent to not providing customization at all. +The `customization` member represents the customization data. Accepted +values are: + +* empty or `undefined`, in which case cSHAKE is equivalent to plain SHAKE +* up to 512 bytes of arbitrary data ### Class: `EcdhKeyDeriveParams` @@ -2472,9 +2485,7 @@ added: v24.8.0 added: v24.15.0 --> -* Type: {number} - -The length of the output in bytes. This must be a positive integer. +* Type: {number} represents the requested output length in bits. #### `kmacParams.customization` diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 12e5c98c27199c..8eae1f6890313e 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -3,11 +3,9 @@ const { ArrayBufferPrototypeSlice, FunctionPrototypeCall, - MathCeil, ObjectDefineProperty, SafeSet, TypedArrayPrototypeGetBuffer, - Uint8Array, } = primordials; const { Buffer } = require('buffer'); @@ -65,7 +63,9 @@ const { getArrayBufferOrView, jobPromise, jobPromiseThen, + numBitsToBytes, toBuf, + truncateToBitLength, kHandle, } = require('internal/crypto/util'); @@ -365,7 +365,6 @@ function diffieHellman(options, callback) { job.run(); } -let masks; // The ecdhDeriveBits function is part of the Web Crypto API and serves both // deriveKeys and deriveBits functions. function ecdhDeriveBits(algorithm, baseKey, length) { @@ -409,27 +408,20 @@ function ecdhDeriveBits(algorithm, baseKey, length) { return bits; 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 sliceLength = numBitsToBytes(length); 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 mod = length % 8; - if (mod === 0) - return slice; - - // eslint-disable-next-line no-sparse-arrays - masks ||= [, 0b10000000, 0b11000000, 0b11100000, 0b11110000, 0b11111000, 0b11111100, 0b11111110]; + if (length % 8 === 0) { + if (byteLength === sliceLength) + return bits; + return ArrayBufferPrototypeSlice(bits, 0, sliceLength); + } - const masked = new Uint8Array(slice); - masked[sliceLength - 1] = masked[sliceLength - 1] & masks[mod]; - return TypedArrayPrototypeGetBuffer(masked); + return TypedArrayPrototypeGetBuffer(truncateToBitLength(length, bits)); }); } diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 58d309f4d86d68..10224a4cd89fc2 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -6,9 +6,11 @@ const { StringPrototypeReplace, StringPrototypeToLowerCase, Symbol, + TypedArrayPrototypeGetBuffer, } = primordials; const { + CShakeJob, Hash: _Hash, HashJob, Hmac: _Hmac, @@ -21,7 +23,10 @@ const { const { getStringOption, jobPromise, + jobPromiseThen, normalizeHashName, + numBitsToBytes, + truncateToBitLength, validateMaxBufferLength, kHandle, getCachedHashId, @@ -222,15 +227,41 @@ function asyncDigest(algorithm, data) { case 'SHA3-384': // Fall through case 'SHA3-512': - // Fall through + return jobPromise(() => new HashJob( + kCryptoJobWebCrypto, + normalizeHashName(algorithm.name), + data)); case 'cSHAKE128': // Fall through - case 'cSHAKE256': - return jobPromise(() => new HashJob( + case 'cSHAKE256': { + const outputLength = algorithm.outputLength; + if (algorithm.functionName?.byteLength || + algorithm.customization?.byteLength) { + if (CShakeJob === undefined) { + throw lazyDOMException( + 'Non-empty CShakeParams functionName or customization is not supported', + 'NotSupportedError'); + } + + return jobPromise(() => new CShakeJob( + kCryptoJobWebCrypto, + algorithm.name, + data, + algorithm.functionName, + algorithm.customization, + outputLength)); + } + + const bits = jobPromise(() => new HashJob( kCryptoJobWebCrypto, normalizeHashName(algorithm.name), data, - algorithm.outputLength)); + numBitsToBytes(outputLength) * 8)); + if (outputLength % 8 === 0) + return bits; + return jobPromiseThen(bits, (bits) => + TypedArrayPrototypeGetBuffer(truncateToBitLength(outputLength, bits))); + } case 'TurboSHAKE128': // Fall through case 'TurboSHAKE256': diff --git a/lib/internal/crypto/mac.js b/lib/internal/crypto/mac.js index 7562231688c7e8..f6f82238b54986 100644 --- a/lib/internal/crypto/mac.js +++ b/lib/internal/crypto/mac.js @@ -18,6 +18,8 @@ const { getUsagesMask, jobPromise, normalizeHashName, + numBitsToBytes, + truncateToBitLength, } = require('internal/crypto/util'); const { @@ -40,6 +42,27 @@ const { const kUsages = ['sign', 'verify']; +function normalizeKeyLength(handle, algorithm) { + let length = handle.getSymmetricKeySize() * 8; + if (length === 0 && algorithm.name === 'HMAC') + throw lazyDOMException('Zero-length key is not supported', 'DataError'); + + if (algorithm.length !== undefined) { + const byteLength = numBitsToBytes(algorithm.length); + if (byteLength !== handle.getSymmetricKeySize()) + throw lazyDOMException('Invalid key length', 'DataError'); + + if (algorithm.length % 8 !== 0) { + handle = importSecretKey( + truncateToBitLength(algorithm.length, handle.export())); + } + + length = algorithm.length; + } + + return { handle, length }; +} + function hmacGenerateKey(algorithm, extractable, usages) { const { hash, @@ -93,7 +116,6 @@ function macImportKey( let length; switch (format) { case 'KeyObjectHandle': { - length = keyData.getSymmetricKeySize() * 8; handle = keyData; break; } @@ -102,7 +124,6 @@ function macImportKey( if (format === 'raw' && !isHmac) { return undefined; } - length = keyData.byteLength * 8; handle = importSecretKey(keyData); break; } @@ -120,20 +141,13 @@ function macImportKey( } handle = importJwkSecretKey(keyData); - length = handle.getSymmetricKeySize() * 8; break; } default: return undefined; } - if (length === 0) - throw lazyDOMException('Zero-length key is not supported', 'DataError'); - - if (algorithm.length !== undefined && - algorithm.length !== length) { - throw lazyDOMException('Invalid key length', 'DataError'); - } + ({ handle, length } = normalizeKeyLength(handle, algorithm)); // eslint-disable-line prefer-const const algorithmObject = { name: algorithm.name, @@ -170,7 +184,8 @@ function kmacSignVerify(key, data, algorithm, signature) { getCryptoKeyHandle(key), algorithm.name, algorithm.customization, - algorithm.outputLength / 8, + getCryptoKeyAlgorithm(key).length, + algorithm.outputLength, data, signature)); } diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index cd55ad9e4b2901..ca02c6907dad73 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -9,6 +9,7 @@ const { DataViewPrototypeGetBuffer, DataViewPrototypeGetByteLength, DataViewPrototypeGetByteOffset, + MathFloor, Number, ObjectDefineProperty, ObjectEntries, @@ -549,6 +550,46 @@ function validateMaxBufferLength(data, name, max = kMaxBufferLength) { } } +/** + * Converts a bit length to the number of bytes needed to contain it. + * Non-byte lengths are rounded up to the next byte. + * @param {number} length + * @returns {number} + */ +function numBitsToBytes(length) { + return MathFloor(length / 8) + MathFloor((7 + (length % 8)) / 8); +} + +/** + * Copies `bytes` up to the byte length needed for `length` bits, then clears + * unused least-significant bits in the final byte. + * @param {number} length + * @param {ArrayBuffer|ArrayBufferView} bytes + * @returns {Uint8Array} + */ +function truncateToBitLength(length, bytes) { + const lengthBytes = numBitsToBytes(length); + const isView = ArrayBufferIsView(bytes); + const byteView = isView ? + new Uint8Array( + getDataViewOrTypedArrayBuffer(bytes), + getDataViewOrTypedArrayByteOffset(bytes), + getDataViewOrTypedArrayByteLength(bytes), + ) : + new Uint8Array(bytes, 0, ArrayBufferPrototypeGetByteLength(bytes)); + const result = TypedArrayPrototypeSlice( + byteView, + 0, + lengthBytes, + ); + + const remainder = length % 8; + if (remainder !== 0) + result[lengthBytes - 1] &= (0xff << (8 - remainder)) & 0xff; + + return result; +} + let webidl; // Keep this as a regular object. The WebIDL converters read and spread these @@ -608,12 +649,19 @@ function normalizeAlgorithm(algorithm, op) { // 3. if (idlType === 'BufferSource' && idlValue) { const isView = ArrayBufferIsView(idlValue); - normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + const idlValueBytes = isView ? new Uint8Array( - isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue, - isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0, - isView ? getDataViewOrTypedArrayByteLength(idlValue) : ArrayBufferPrototypeGetByteLength(idlValue), - ), + getDataViewOrTypedArrayBuffer(idlValue), + getDataViewOrTypedArrayByteOffset(idlValue), + getDataViewOrTypedArrayByteLength(idlValue), + ) : + new Uint8Array( + idlValue, + 0, + ArrayBufferPrototypeGetByteLength(idlValue), + ); + normalizedAlgorithm[member] = TypedArrayPrototypeSlice( + idlValueBytes, ); } else if (idlType === 'HashAlgorithmIdentifier') { normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest'); @@ -1003,6 +1051,8 @@ module.exports = { cleanupWebCryptoResult, prepareWebCryptoResult, validateMaxBufferLength, + numBitsToBytes, + truncateToBitLength, bigIntArrayToUnsignedBigInt, bigIntArrayToUnsignedInt, getBlockSize, diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index aa62c66a78502f..3c1dd9e27d78a8 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -24,6 +24,7 @@ const { } = primordials; const { + CShakeJob, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatSPKI, @@ -66,6 +67,7 @@ const { jobPromiseThen, normalizeAlgorithm, normalizeHashName, + numBitsToBytes, prepareWebCryptoResult, validateMaxBufferLength, } = require('internal/crypto/util'); @@ -1749,7 +1751,7 @@ class SubtleCrypto { case 'KMAC128': case 'KMAC256': if (normalizedAdditionalAlgorithm.length === undefined || - normalizedAdditionalAlgorithm.length === 256) { + numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) { break; } return false; @@ -1793,7 +1795,15 @@ function check(op, alg, length) { switch (op) { case 'decapsulate': case 'decrypt': - case 'digest': + case 'digest': { + if ((normalizedAlgorithm.name === 'cSHAKE128' || + normalizedAlgorithm.name === 'cSHAKE256') && + (normalizedAlgorithm.functionName?.byteLength || + normalizedAlgorithm.customization?.byteLength)) { + return CShakeJob !== undefined; + } + return true; + } case 'encapsulate': case 'encrypt': case 'exportKey': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index cca3a5fb044d92..dbdb7a5ea47cb6 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -1,18 +1,24 @@ 'use strict'; const { + ArrayBufferIsView, ArrayPrototypeIncludes, MathPow, NumberParseInt, ObjectPrototypeHasOwnProperty, + StringPrototypeCharCodeAt, StringPrototypeStartsWith, StringPrototypeToLowerCase, + Uint8Array, } = primordials; const { lazyDOMException, kEmptyObject, } = require('internal/util'); +const { + isUint32, +} = require('internal/validators'); const { CryptoKey } = require('internal/crypto/webcrypto'); const { getCryptoKeyAlgorithm, @@ -21,6 +27,7 @@ const { const { validateMaxBufferLength, kNamedCurveAliases, + numBitsToBytes, } = require('internal/crypto/util'); const { converters: webidl, @@ -256,6 +263,43 @@ function validateZeroLength(parameterName) { }; } +function validateCShakeOutputLength(V) { + if (!isUint32(numBitsToBytes(V) * 8)) { + throw lazyDOMException( + 'Invalid CShakeParams outputLength', + 'OperationError'); + } +} + +function bufferSourceEqualsAscii(V, string) { + if (V.byteLength !== string.length) return false; + + const bytes = ArrayBufferIsView(V) ? + new Uint8Array(V.buffer, V.byteOffset, V.byteLength) : + new Uint8Array(V); + for (let i = 0; i < bytes.length; i++) { + if (bytes[i] !== StringPrototypeCharCodeAt(string, i)) return false; + } + return true; +} + +function validateCShakeFunctionName(V) { + if (V.byteLength === 0 || + bufferSourceEqualsAscii(V, 'KMAC') || + bufferSourceEqualsAscii(V, 'TupleHash') || + bufferSourceEqualsAscii(V, 'ParallelHash')) { + return; + } + + throw lazyDOMException( + 'Unsupported CShakeParams functionName', + 'NotSupportedError'); +} + +function validateCShakeCustomization(V) { + validateMaxBufferLength(V, 'CShakeParams.customization', 512); +} + converters.RsaPssParams = createDictionaryConverter( 'RsaPssParams', [ dictAlgorithm, @@ -293,6 +337,13 @@ converters.EcdsaParams = createDictionaryConverter( ], ]); +function validateHmacKeyLength(parameterName, zeroError) { + return (V) => { + if (V === 0) + throw lazyDOMException(`${parameterName} cannot be 0`, zeroError); + }; +} + for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], ['HmacImportParams', 'DataError']]) { converters[name] = createDictionaryConverter( name, [ @@ -308,7 +359,7 @@ for (const { 0: name, 1: zeroError } of [['HmacKeyGenParams', 'OperationError'], key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: validateMacKeyLength(`${name}.length`, zeroError), + validator: validateHmacKeyLength(`${name}.length`, zeroError), }, ], ]); @@ -390,23 +441,18 @@ converters.CShakeParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - 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'); - }, + validator: validateCShakeOutputLength, required: true, }, { key: 'functionName', converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.functionName'), + validator: validateCShakeFunctionName, }, { key: 'customization', converter: converters.BufferSource, - validator: validateZeroLength('CShakeParams.customization'), + validator: validateCShakeCustomization, }, ], ]); @@ -680,18 +726,7 @@ converters.Argon2Params = createDictionaryConverter( ], ]); -function validateMacKeyLength(parameterName, zeroError) { - return (V) => { - if (V === 0) - throw lazyDOMException(`${parameterName} cannot be 0`, zeroError); - - // The Web Crypto spec allows for key lengths that are not multiples of 8. We don't. - if (V % 8) - throw lazyDOMException(`Unsupported ${parameterName}`, 'NotSupportedError'); - }; -} - -for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], ['KmacImportParams', 'DataError']]) { +for (const name of ['KmacKeyGenParams', 'KmacImportParams']) { converters[name] = createDictionaryConverter( name, [ dictAlgorithm, @@ -700,7 +735,6 @@ for (const { 0: name, 1: zeroError } of [['KmacKeyGenParams', 'OperationError'], key: 'length', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - validator: validateMacKeyLength(`${name}.length`, zeroError), }, ], ]); @@ -714,11 +748,6 @@ converters.KmacParams = createDictionaryConverter( key: 'outputLength', converter: (V, opts) => converters['unsigned long'](V, enforceRangeOptions(opts)), - 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, }, { diff --git a/src/crypto/crypto_hash.cc b/src/crypto/crypto_hash.cc index 58c4a45986bf0f..946f6850d5e68f 100644 --- a/src/crypto/crypto_hash.cc +++ b/src/crypto/crypto_hash.cc @@ -7,11 +7,23 @@ #include "threadpoolwork-inl.h" #include "v8.h" +#if OPENSSL_WITH_EVP_MAC +#include +#include +#endif + #if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK #include #endif +#include +#include +#include #include +#include +#include +#include +#include namespace node { @@ -351,6 +363,9 @@ void Hash::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "oneShotDigest", OneShotDigest); HashJob::Initialize(env, target); +#if OPENSSL_WITH_EVP_MAC + CShakeJob::Initialize(env, target); +#endif } void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { @@ -362,6 +377,9 @@ void Hash::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(OneShotDigest); HashJob::RegisterExternalReferences(registry); +#if OPENSSL_WITH_EVP_MAC + CShakeJob::RegisterExternalReferences(registry); +#endif } // new Hash(algorithm, algorithmId, xofLen, algorithmCache) @@ -544,8 +562,8 @@ Maybe HashTraits::AdditionalConfig( if (args[offset + 2]->IsUint32()) [[unlikely]] { // length is expressed in terms of bits params->length = - static_cast(args[offset + 2] - .As()->Value()) / CHAR_BIT; + static_cast(args[offset + 2].As()->Value()) / + CHAR_BIT; if (params->length != expected) { if ((EVP_MD_flags(params->digest) & EVP_MD_FLAG_XOF) == 0) [[unlikely]] { THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Digest method not supported"); @@ -580,5 +598,346 @@ bool HashTraits::DeriveBits(Environment* env, return true; } +#if OPENSSL_WITH_EVP_MAC +namespace { + +static constexpr std::array kEmptyString = {}; +static constexpr size_t kKeccakKmac128Rate = 168; +static constexpr size_t kKeccakKmac256Rate = 136; +static constexpr size_t kMaxCShakeCustomizationSize = 512; + +struct EncodedLength { + std::array data; + size_t size; +}; + +struct EncodedStringInput { + const void* data; + size_t byte_length; + size_t bit_length; +}; + +struct KeccakKmacXof { + ncrypto::EVPMDCtxPointer ctx; + size_t rate; +}; + +size_t EncodedLengthSize(size_t value) { + size_t size = 1; + size_t remaining = value; + while (remaining >>= CHAR_BIT) size++; + return size + 1; +} + +bool AddSize(size_t a, size_t b, size_t* out) { + if (a > std::numeric_limits::max() - b) return false; + *out = a + b; + return true; +} + +EncodedLength EncodeLength(size_t value, bool left) { + const size_t value_size = EncodedLengthSize(value) - 1; + EncodedLength encoded = {{}, value_size + 1}; + + if (left) encoded.data[0] = static_cast(value_size); + for (size_t n = 0; n < value_size; n++) { + const size_t shift = CHAR_BIT * (value_size - n - 1); + encoded.data[(left ? 1 : 0) + n] = + static_cast(value >> shift); + } + if (!left) encoded.data[value_size] = static_cast(value_size); + + return encoded; +} + +bool DigestUpdate(ncrypto::EVPMDCtxPointer* ctx, + const void* data, + size_t size) { + if (size == 0) return true; + return ctx->digestUpdate(ncrypto::Buffer{ + .data = data, + .len = size, + }); +} + +bool DigestUpdateZeros(ncrypto::EVPMDCtxPointer* ctx, size_t size) { + static constexpr std::array zeros = {}; + while (size > 0) { + const size_t chunk = std::min(size, zeros.size()); + if (!DigestUpdate(ctx, zeros.data(), chunk)) return false; + size -= chunk; + } + return true; +} + +bool EncodedStringSize(size_t byte_length, size_t bit_length, size_t* size) { + return AddSize(EncodedLengthSize(bit_length), byte_length, size); +} + +bool ByteLengthToBitLength(size_t byte_length, size_t* bit_length) { + if (byte_length > std::numeric_limits::max() / CHAR_BIT) { + return false; + } + *bit_length = byte_length * CHAR_BIT; + return true; +} + +KeccakKmacXof NewKeccakKmacXof(bool use_128_bits) { + // OpenSSL 3.x exposes the cSHAKE/KMAC suffix primitive as KECCAK-KMAC-*. + const char* digest_name = use_128_bits ? OSSL_DIGEST_NAME_KECCAK_KMAC128 + : OSSL_DIGEST_NAME_KECCAK_KMAC256; + auto digest = std::unique_ptr{ + EVP_MD_fetch(nullptr, digest_name, nullptr), EVP_MD_free}; + if (!digest) return {}; + + auto ctx = ncrypto::EVPMDCtxPointer::New(); + if (!ctx.digestInit(digest.get())) return {}; + + return { + .ctx = std::move(ctx), + .rate = use_128_bits ? kKeccakKmac128Rate : kKeccakKmac256Rate, + }; +} + +bool ToEncodedStringInput(const void* data, + size_t byte_length, + EncodedStringInput* input) { + if (byte_length > 0 && data == nullptr) return false; + + size_t bit_length; + if (!ByteLengthToBitLength(byte_length, &bit_length)) return false; + + *input = { + .data = byte_length == 0 ? kEmptyString.data() : data, + .byte_length = byte_length, + .bit_length = bit_length, + }; + return true; +} + +bool DigestUpdateEncodedLength(ncrypto::EVPMDCtxPointer* ctx, + size_t value, + bool left) { + const EncodedLength encoded = EncodeLength(value, left); + return DigestUpdate(ctx, encoded.data.data(), encoded.size); +} + +bool DigestUpdateEncodedString(ncrypto::EVPMDCtxPointer* ctx, + const void* data, + size_t byte_length, + size_t bit_length) { + return DigestUpdateEncodedLength(ctx, bit_length, true) && + DigestUpdate(ctx, data, byte_length); +} + +bool DigestUpdateBytepad(ncrypto::EVPMDCtxPointer* ctx, + size_t width, + const void* data, + size_t byte_length, + size_t bit_length, + const void* data2 = nullptr, + size_t byte_length2 = 0, + size_t bit_length2 = 0) { + if (width == 0) return false; + + size_t encoded_size; + size_t written = EncodedLengthSize(width); + if (!EncodedStringSize(byte_length, bit_length, &encoded_size) || + !AddSize(written, encoded_size, &written)) { + return false; + } + if (data2 != nullptr) { + if (!EncodedStringSize(byte_length2, bit_length2, &encoded_size) || + !AddSize(written, encoded_size, &written)) { + return false; + } + } + + size_t padded_size; + if (!AddSize(written, width - 1, &padded_size)) return false; + padded_size = padded_size / width * width; + DCHECK_GE(padded_size, written); + const size_t padding = padded_size - written; + + return DigestUpdateEncodedLength(ctx, width, true) && + DigestUpdateEncodedString(ctx, data, byte_length, bit_length) && + (data2 == nullptr || + DigestUpdateEncodedString(ctx, data2, byte_length2, bit_length2)) && + DigestUpdateZeros(ctx, padding); +} + +} // namespace + +CShakeConfig::CShakeConfig(CShakeConfig&& other) noexcept + : mode(other.mode), + in(std::move(other.in)), + function_name(std::move(other.function_name)), + customization(std::move(other.customization)), + variant(other.variant), + length(other.length) {} + +CShakeConfig& CShakeConfig::operator=(CShakeConfig&& other) noexcept { + if (&other == this) return *this; + this->~CShakeConfig(); + return *new (this) CShakeConfig(std::move(other)); +} + +void CShakeConfig::MemoryInfo(MemoryTracker* tracker) const { + // If the Job is sync, then the CShakeConfig does not own the data. + if (IsCryptoJobAsync(mode)) { + tracker->TrackFieldWithSize("in", in.size()); + tracker->TrackFieldWithSize("function_name", function_name.size()); + tracker->TrackFieldWithSize("customization", customization.size()); + } +} + +MaybeLocal CShakeTraits::EncodeOutput(Environment* env, + const CShakeConfig& params, + ByteSource* out) { + return out->ToArrayBuffer(env); +} + +Maybe CShakeTraits::AdditionalConfig( + CryptoJobMode mode, + const FunctionCallbackInfo& args, + unsigned int offset, + CShakeConfig* params) { + Environment* env = Environment::GetCurrent(args); + + params->mode = mode; + + CHECK(args[offset]->IsString()); // Algorithm name + Utf8Value algorithm_name(env->isolate(), args[offset]); + std::string_view algorithm_str = algorithm_name.ToStringView(); + + if (algorithm_str == "cSHAKE128") { + params->variant = CShakeVariant::CSHAKE128; + } else if (algorithm_str == "cSHAKE256") { + params->variant = CShakeVariant::CSHAKE256; + } else { + UNREACHABLE(); + } + + ArrayBufferOrViewContents data(args[offset + 1]); + if (!data.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "data is too big"); + return Nothing(); + } + params->in = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); + + if (!args[offset + 2]->IsUndefined()) { + ArrayBufferOrViewContents function_name(args[offset + 2]); + if (!function_name.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "functionName is too big"); + return Nothing(); + } + params->function_name = IsCryptoJobAsync(mode) + ? function_name.ToCopy() + : function_name.ToByteSource(); + } + + if (!args[offset + 3]->IsUndefined()) { + ArrayBufferOrViewContents customization(args[offset + 3]); + if (!customization.CheckSizeInt32()) [[unlikely]] { + THROW_ERR_OUT_OF_RANGE(env, "customization is too big"); + return Nothing(); + } + params->customization = IsCryptoJobAsync(mode) + ? customization.ToCopy() + : customization.ToByteSource(); + } + + CHECK(args[offset + 4]->IsUint32()); // Length + params->length = args[offset + 4].As()->Value(); + + return JustVoid(); +} + +bool CShakeTraits::DeriveBits(Environment* env, + const CShakeConfig& params, + ByteSource* out, + CryptoJobMode mode) { + CShakeParams cshake_params = { + .variant = params.variant, + .function_name_data = params.function_name.data(), + .function_name_size = params.function_name.size(), + .customization_data = params.customization.data(), + .customization_size = params.customization.size(), + .bytepad_input = nullptr, + .input_data = params.in.data(), + .input_size = params.in.size(), + .append_output_length = false, + .length = params.length, + }; + return DeriveCShakeBits(cshake_params, out); +} + +bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out) { + if (params.customization_size > kMaxCShakeCustomizationSize) { + return false; + } + + if (params.length == 0) { + *out = ByteSource(); + return true; + } + + auto xof = NewKeccakKmacXof(params.variant == CShakeVariant::CSHAKE128); + if (!xof.ctx) return false; + auto ctx = std::move(xof.ctx); + + EncodedStringInput function_name; + EncodedStringInput customization; + if (!ToEncodedStringInput(params.function_name_data, + params.function_name_size, + &function_name) || + !ToEncodedStringInput(params.customization_data, + params.customization_size, + &customization)) { + return false; + } + + if (!DigestUpdateBytepad(&ctx, + xof.rate, + function_name.data, + function_name.byte_length, + function_name.bit_length, + customization.data, + customization.byte_length, + customization.bit_length)) { + return false; + } + + if (params.bytepad_input != nullptr && + !DigestUpdateBytepad(&ctx, + xof.rate, + params.bytepad_input->data, + params.bytepad_input->byte_length, + params.bytepad_input->bit_length)) { + return false; + } + + if (!DigestUpdate(&ctx, params.input_data, params.input_size)) { + return false; + } + + if (params.append_output_length && + !DigestUpdateEncodedLength(&ctx, params.length, false)) { + return false; + } + + const size_t length_bytes = + NumBitsToBytes(static_cast(params.length)); + auto data = ctx.digestFinal(length_bytes); + if (!data) [[unlikely]] + return false; + + DCHECK(!data.isSecure()); + *out = ByteSource::Allocated(data.release()); + if (params.length % CHAR_BIT != 0) TruncateToBitLength(params.length, out); + return true; +} +#endif // OPENSSL_WITH_EVP_MAC + } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_hash.h b/src/crypto/crypto_hash.h index 0a839733cc156e..d9fe46f37b3190 100644 --- a/src/crypto/crypto_hash.h +++ b/src/crypto/crypto_hash.h @@ -82,6 +82,74 @@ struct HashTraits final { using HashJob = DeriveBitsJob; +#if OPENSSL_WITH_EVP_MAC +enum class CShakeVariant { CSHAKE128, CSHAKE256 }; + +struct CShakeBytepadInput final { + const void* data; + size_t byte_length; + size_t bit_length; +}; + +struct CShakeParams final { + CShakeVariant variant; + const void* function_name_data; + size_t function_name_size; + const void* customization_data; + size_t customization_size; + const CShakeBytepadInput* bytepad_input; + const void* input_data; + size_t input_size; + bool append_output_length; + uint32_t length; // Output length in bits +}; + +bool DeriveCShakeBits(const CShakeParams& params, ByteSource* out); + +struct CShakeConfig final : public MemoryRetainer { + CryptoJobMode mode; + ByteSource in; + ByteSource function_name; + ByteSource customization; + CShakeVariant variant; + uint32_t length; // Output length in bits + + CShakeConfig() = default; + + explicit CShakeConfig(CShakeConfig&& other) noexcept; + + CShakeConfig& operator=(CShakeConfig&& other) noexcept; + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_MEMORY_INFO_NAME(CShakeConfig) + SET_SELF_SIZE(CShakeConfig) +}; + +struct CShakeTraits final { + using AdditionalParameters = CShakeConfig; + static constexpr const char* JobName = "CShakeJob"; + static constexpr AsyncWrap::ProviderType Provider = + AsyncWrap::PROVIDER_HASHREQUEST; + + static v8::Maybe AdditionalConfig( + CryptoJobMode mode, + const v8::FunctionCallbackInfo& args, + unsigned int offset, + CShakeConfig* params); + + static bool DeriveBits(Environment* env, + const CShakeConfig& params, + ByteSource* out, + CryptoJobMode mode); + + static v8::MaybeLocal EncodeOutput(Environment* env, + const CShakeConfig& params, + ByteSource* out); +}; + +using CShakeJob = DeriveBitsJob; +#endif // OPENSSL_WITH_EVP_MAC + } // namespace crypto } // namespace node diff --git a/src/crypto/crypto_keygen.cc b/src/crypto/crypto_keygen.cc index 597c8bab781fc0..24b0f5aca2b4b3 100644 --- a/src/crypto/crypto_keygen.cc +++ b/src/crypto/crypto_keygen.cc @@ -64,7 +64,13 @@ Maybe SecretKeyGenTraits::AdditionalConfig( SecretKeyGenConfig* params) { CHECK(args[*offset]->IsUint32()); uint32_t bits = args[*offset].As()->Value(); - params->length = bits / CHAR_BIT; + params->length_bits = bits; + if (mode == kCryptoJobWebCrypto) { + params->length = NumBitsToBytes(static_cast(bits)); + params->truncate_to_bit_length = bits % CHAR_BIT != 0; + } else { + params->length = bits / CHAR_BIT; + } *offset += 1; return JustVoid(); } @@ -77,6 +83,8 @@ KeyGenJobStatus SecretKeyGenTraits::DoKeyGen(Environment* env, return KeyGenJobStatus::FAILED; } params->out = ByteSource::Allocated(bytes.release()); + if (params->truncate_to_bit_length) + TruncateToBitLength(params->length_bits, ¶ms->out); return KeyGenJobStatus::OK; } diff --git a/src/crypto/crypto_keygen.h b/src/crypto/crypto_keygen.h index 1702dfabb4af2a..61421bfe5c11d7 100644 --- a/src/crypto/crypto_keygen.h +++ b/src/crypto/crypto_keygen.h @@ -284,8 +284,10 @@ struct KeyPairGenTraits final { }; struct SecretKeyGenConfig final : public MemoryRetainer { - size_t length; // In bytes. - ByteSource out; // Placeholder for the generated key bytes. + size_t length = 0; // In bytes. + size_t length_bits = 0; + bool truncate_to_bit_length = false; + ByteSource out; // Placeholder for the generated key bytes. void MemoryInfo(MemoryTracker* tracker) const override; SET_MEMORY_INFO_NAME(SecretKeyGenConfig) diff --git a/src/crypto/crypto_kmac.cc b/src/crypto/crypto_kmac.cc index e041ecc5d74b53..5f151c9a62a66e 100644 --- a/src/crypto/crypto_kmac.cc +++ b/src/crypto/crypto_kmac.cc @@ -1,11 +1,15 @@ #include "crypto/crypto_kmac.h" #include "async_wrap-inl.h" +#include "crypto/crypto_hash.h" #include "node_internals.h" #include "threadpoolwork-inl.h" #if OPENSSL_WITH_EVP_MAC #include #include +#include +#include +#include #include "crypto/crypto_keys.h" #include "crypto/crypto_sig.h" #include "ncrypto.h" @@ -22,6 +26,7 @@ using v8::Local; using v8::Maybe; using v8::MaybeLocal; using v8::Nothing; +using v8::Number; using v8::Object; using v8::Uint32; using v8::Value; @@ -34,6 +39,7 @@ KmacConfig::KmacConfig(KmacConfig&& other) noexcept signature(std::move(other.signature)), customization(std::move(other.customization)), variant(other.variant), + key_length(other.key_length), length(other.length) {} KmacConfig& KmacConfig::operator=(KmacConfig&& other) noexcept { @@ -96,18 +102,27 @@ Maybe KmacTraits::AdditionalConfig( } // If undefined, params->customization remains uninitialized (size 0). - CHECK(args[offset + 4]->IsUint32()); // Length - params->length = args[offset + 4].As()->Value(); + CHECK(args[offset + 4]->IsNumber()); // Key length + double key_length = args[offset + 4].As()->Value(); + if (!(key_length >= 0) || + key_length > static_cast(std::numeric_limits::max())) { + THROW_ERR_OUT_OF_RANGE(env, "key length is too big"); + return Nothing(); + } + params->key_length = static_cast(key_length); + + CHECK(args[offset + 5]->IsUint32()); // Length + params->length = args[offset + 5].As()->Value(); - 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 = IsCryptoJobAsync(mode) ? data.ToCopy() : data.ToByteSource(); - if (!args[offset + 6]->IsUndefined()) { - ArrayBufferOrViewContents signature(args[offset + 6]); + if (!args[offset + 7]->IsUndefined()) { + ArrayBufferOrViewContents signature(args[offset + 7]); if (!signature.CheckSizeInt32()) [[unlikely]] { THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); return Nothing(); @@ -119,23 +134,83 @@ Maybe KmacTraits::AdditionalConfig( return JustVoid(); } +namespace { + +static constexpr std::array kKmacFunctionName = { + 'K', 'M', 'A', 'C'}; +static constexpr size_t kKmacMinOpenSSLKeySize = 4; +// Keep the bit-aware path within OpenSSL's KMAC provider limits. +static constexpr size_t kKmacMaxOpenSSLKeySize = 512; +static constexpr size_t kKmacMaxOpenSSLCustomizationSize = 512; +static constexpr size_t kKmacMaxOpenSSLOutputSize = 0xffffff / CHAR_BIT; + +bool KmacParamsWithinOpenSSLLimits(const KmacConfig& params, + size_t key_size, + size_t length_bytes) { + return key_size <= kKmacMaxOpenSSLKeySize && + NumBitsToBytes(params.key_length) <= kKmacMaxOpenSSLKeySize && + params.customization.size() <= kKmacMaxOpenSSLCustomizationSize && + length_bytes <= kKmacMaxOpenSSLOutputSize; +} + +bool DeriveBitsWithCShake(const KmacConfig& params, + const void* key_data, + size_t key_size, + ByteSource* out) { + const size_t key_length_bytes = NumBitsToBytes(params.key_length); + if (key_size < key_length_bytes) return false; + + CShakeBytepadInput key_input = { + .data = key_data, + .byte_length = key_length_bytes, + .bit_length = params.key_length, + }; + CShakeParams cshake_params = { + .variant = params.variant == KmacVariant::KMAC128 + ? CShakeVariant::CSHAKE128 + : CShakeVariant::CSHAKE256, + .function_name_data = kKmacFunctionName.data(), + .function_name_size = kKmacFunctionName.size(), + .customization_data = params.customization.data(), + .customization_size = params.customization.size(), + .bytepad_input = &key_input, + .input_data = params.data.data(), + .input_size = params.data.size(), + .append_output_length = true, + .length = params.length, + }; + return DeriveCShakeBits(cshake_params, out); +} + +} // namespace + bool KmacTraits::DeriveBits(Environment* env, const KmacConfig& params, ByteSource* out, CryptoJobMode mode) { - if (params.length == 0) { - *out = ByteSource(); - return true; - } + const bool truncate_to_bit_length = params.length % CHAR_BIT != 0; + const size_t length_bytes = + NumBitsToBytes(static_cast(params.length)); // Get the key data. const void* key_data = params.key.GetSymmetricKey(); size_t key_size = params.key.GetSymmetricKeySize(); - if (key_size == 0) { + if (!KmacParamsWithinOpenSSLLimits(params, key_size, length_bytes)) { return false; } + if (params.length == 0) { + *out = ByteSource(); + return true; + } + + // OpenSSL's EVP_MAC provider rejects KMAC keys shorter than 4 bytes. + if (params.length % CHAR_BIT != 0 || params.key_length % CHAR_BIT != 0 || + key_size < kKmacMinOpenSSLKeySize) { + return DeriveBitsWithCShake(params, key_data, key_size, out); + } + // Fetch the KMAC algorithm auto mac = EVPMacPointer::Fetch((params.variant == KmacVariant::KMAC128) ? OSSL_MAC_NAME_KMAC128 @@ -155,7 +230,7 @@ bool KmacTraits::DeriveBits(Environment* env, size_t params_count = 0; // Set output length (always required for KMAC). - size_t outlen = params.length; + size_t outlen = length_bytes; params_array[params_count++] = OSSL_PARAM_construct_size_t(OSSL_MAC_PARAM_SIZE, &outlen); @@ -182,13 +257,14 @@ bool KmacTraits::DeriveBits(Environment* env, } // Finalize and get the result. - auto result = mac_ctx.final(params.length); + auto result = mac_ctx.final(length_bytes); if (!result) { return false; } auto buffer = result.release(); *out = ByteSource::Allocated(buffer.data, buffer.len); + if (truncate_to_bit_length) TruncateToBitLength(params.length, out); return true; } diff --git a/src/crypto/crypto_kmac.h b/src/crypto/crypto_kmac.h index 05b9077fbf35e0..40aa83580d0f2d 100644 --- a/src/crypto/crypto_kmac.h +++ b/src/crypto/crypto_kmac.h @@ -22,7 +22,8 @@ struct KmacConfig final : public MemoryRetainer { ByteSource signature; ByteSource customization; KmacVariant variant; - uint32_t length; // Output length in bytes + size_t key_length; // Key length in bits + uint32_t length; // Output length in bits KmacConfig() = default; diff --git a/src/crypto/crypto_util.cc b/src/crypto/crypto_util.cc index e5c1c254179033..06c4aa87464de6 100644 --- a/src/crypto/crypto_util.cc +++ b/src/crypto/crypto_util.cc @@ -344,6 +344,30 @@ ByteSource& ByteSource::operator=(ByteSource&& other) noexcept { return *this; } +void TruncateToBitLength(size_t length_bits, ByteSource* bytes) { + CHECK_NOT_NULL(bytes); + const size_t length_bytes = NumBitsToBytes(length_bits); + CHECK_LE(length_bytes, bytes->size()); + + if (bytes->allocated_data_ == nullptr || bytes->size() != length_bytes) { + auto data = DataPointer::Alloc(length_bytes); + if (length_bytes > 0) { + CHECK_NOT_NULL(data.get()); + memcpy(data.get(), bytes->data(), length_bytes); + } + *bytes = ByteSource::Allocated(data.release()); + } + + const size_t remainder_bits = length_bits % CHAR_BIT; + if (remainder_bits != 0) { + auto* data = static_cast(bytes->allocated_data_); + CHECK_NOT_NULL(data); + const unsigned char mask = + static_cast(0xff << (CHAR_BIT - remainder_bits)); + data[length_bytes - 1] &= mask; + } +} + std::unique_ptr ByteSource::ReleaseToBackingStore( Environment* env) { // It's ok for allocated_data_ to be nullptr but diff --git a/src/crypto/crypto_util.h b/src/crypto/crypto_util.h index 16d39a72a59b88..a99ab94fff5d6b 100644 --- a/src/crypto/crypto_util.h +++ b/src/crypto/crypto_util.h @@ -43,6 +43,11 @@ constexpr size_t kSizeOf_HMAC_CTX = 32; constexpr size_t kSizeOf_SSL_CTX = 240; constexpr size_t kSizeOf_X509 = 128; +template +constexpr T NumBitsToBytes(T bits) { + return (bits / CHAR_BIT) + ((CHAR_BIT - 1 + (bits % CHAR_BIT)) / CHAR_BIT); +} + bool ProcessFipsOptions(); bool InitCryptoOnce(v8::Isolate* isolate); @@ -231,6 +236,8 @@ class ByteSource final { Environment* env, v8::Local value); private: + friend void TruncateToBitLength(size_t length_bits, ByteSource* bytes); + const void* data_ = nullptr; void* allocated_data_ = nullptr; size_t size_ = 0; @@ -239,6 +246,8 @@ class ByteSource final { : data_(data), allocated_data_(allocated_data), size_(size) {} }; +void TruncateToBitLength(size_t length_bits, ByteSource* bytes); + enum CryptoJobMode { kCryptoJobAsync, kCryptoJobSync, kCryptoJobWebCrypto }; CryptoJobMode GetCryptoJobMode(v8::Local args); diff --git a/test/fixtures/crypto/kmac.js b/test/fixtures/crypto/kmac.js index cc1870af2bb04f..ed265bb9c9f3e5 100644 --- a/test/fixtures/crypto/kmac.js +++ b/test/fixtures/crypto/kmac.js @@ -114,6 +114,61 @@ module.exports = function() { 0x76, 0xfc, 0x89, 0x65, ]), }, + { + // KMAC128 with a short key, generated with OpenSSL's KECCAK-KMAC128 + // digest over independently encoded NIST SP 800-185 framing. + algorithm: 'KMAC128', + key: Buffer.from([0x00, 0x01, 0x02]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: Buffer.from('Node.js'), + outputLength: 256, + expected: Buffer.from([ + 0xfb, 0x7c, 0xbb, 0xa2, 0xa1, 0x0d, 0x1a, 0x87, 0x9a, 0x9f, 0x96, 0x8c, + 0x58, 0x9d, 0x2a, 0xfe, 0x4a, 0x9b, 0xbf, 0x03, 0x7e, 0x85, 0x2c, 0xac, + 0x05, 0xdd, 0x78, 0x5b, 0x78, 0xd6, 0x57, 0x1c, + ]), + }, + { + // KMAC256 with a short key, generated with OpenSSL's KECCAK-KMAC256 + // digest over independently encoded NIST SP 800-185 framing. + algorithm: 'KMAC256', + key: Buffer.from([0x00, 0x01, 0x02]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: Buffer.from('Node.js'), + outputLength: 512, + expected: Buffer.from([ + 0x2c, 0xcf, 0x20, 0xde, 0xd8, 0xc9, 0x6d, 0xb0, 0x5f, 0x15, 0xe0, 0xb3, + 0xce, 0x5d, 0xf0, 0x45, 0xc7, 0xd7, 0x4e, 0xfe, 0x18, 0xee, 0x36, 0xa8, + 0xe4, 0x9a, 0x37, 0xfb, 0xc2, 0xb1, 0xbb, 0xfd, 0xad, 0xf5, 0xb7, 0x89, + 0xd3, 0xa4, 0xbc, 0xb3, 0xa8, 0x28, 0x8e, 0x9f, 0x25, 0xe6, 0x8d, 0x5b, + 0x4a, 0x01, 0x0b, 0x90, 0xae, 0x6d, 0x2b, 0xfc, 0xf1, 0xb6, 0xbb, 0x82, + 0x34, 0x8b, 0x51, 0xd9, + ]), + }, + { + // KMAC128 with a non-byte-aligned output length. The second byte has its + // unused low bits cleared after squeezing a 9-bit result. + algorithm: 'KMAC128', + key: Buffer.from([0x00, 0x01, 0x02, 0x03]), + data: Buffer.from([0x01, 0x02, 0x03]), + customization: undefined, + outputLength: 9, + expected: Buffer.from([0x63, 0x80]), + }, + { + // KMAC128 with a non-byte-aligned key length. The raw key is already + // truncated to 25 bits, matching WebCrypto import semantics. + algorithm: 'KMAC128', + key: Buffer.from([0xff, 0xff, 0xff, 0x80]), + keyLength: 25, + data: Buffer.from([0x01, 0x02, 0x03]), + customization: undefined, + outputLength: 128, + expected: Buffer.from([ + 0x25, 0xea, 0xc7, 0x06, 0x82, 0x47, 0x7e, 0x3c, 0x9b, 0xf0, 0xf1, 0x51, + 0x87, 0x46, 0x40, 0x0c, + ]), + }, ]; return vectors; diff --git a/test/fixtures/webcrypto/supports-level-2.mjs b/test/fixtures/webcrypto/supports-level-2.mjs index 1850acacdc22e9..674e4cf8c6de1a 100644 --- a/test/fixtures/webcrypto/supports-level-2.mjs +++ b/test/fixtures/webcrypto/supports-level-2.mjs @@ -62,7 +62,7 @@ export const vectors = { [true, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }], @@ -78,7 +78,7 @@ export const vectors = { [false, { name: 'AES-KW', length: 25 }], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 0 }], ], 'deriveKey': [ @@ -121,7 +121,7 @@ export const vectors = { [false, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256' }], - [false, + [true, { name: 'ECDH', public: ECDH.publicKey }, { name: 'HMAC', hash: 'SHA-256', length: 255 }], [true, @@ -183,7 +183,7 @@ export const vectors = { [true, 'Ed25519'], [true, { name: 'HMAC', hash: 'SHA-256' }], [true, { name: 'HMAC', hash: 'SHA-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA-256', length: 25 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }], [true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }], @@ -195,7 +195,7 @@ export const vectors = { [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 }], + [true, { name: 'HMAC', hash: 'SHA-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA-256', length: 0 }], [true, 'HKDF'], [true, 'PBKDF2'], diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 2d370b8e21d3d5..a3e5fc54976483 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -18,15 +18,19 @@ export const vectors = { [false, 'cSHAKE128'], [shake128, { name: 'cSHAKE128', outputLength: 128 }], [shake128, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE128', outputLength: 127 }], + [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('KMAC') }], + [false, { name: 'cSHAKE128', outputLength: 128, functionName: Buffer.from('SHAKE') }], + [shake128 && kmac, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE128', outputLength: 128, customization: Buffer.alloc(513) }], + [shake128, { name: 'cSHAKE128', outputLength: 127 }], [false, 'cSHAKE256'], [shake256, { name: 'cSHAKE256', outputLength: 256 }], [shake256, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(0), customization: Buffer.alloc(0) }], - [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], - [false, { name: 'cSHAKE256', outputLength: 255 }], + [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('KMAC') }], + [false, { name: 'cSHAKE256', outputLength: 256, functionName: Buffer.from('SHAKE') }], + [shake256 && kmac, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(1) }], + [false, { name: 'cSHAKE256', outputLength: 256, customization: Buffer.alloc(513) }], + [shake256, { name: 'cSHAKE256', outputLength: 255 }], [false, 'TurboSHAKE128'], [true, { name: 'TurboSHAKE128', outputLength: 128 }], [true, { name: 'TurboSHAKE128', outputLength: 128, domainSeparation: 0x07 }], @@ -68,7 +72,9 @@ export const vectors = { [false, 'KMAC128'], [false, 'KMAC256'], [kmac, { name: 'KMAC128', outputLength: 256 }], + [kmac, { name: 'KMAC128', outputLength: 255 }], [kmac, { name: 'KMAC256', outputLength: 256 }], + [kmac, { name: 'KMAC256', outputLength: 255 }], ], 'generateKey': [ [pqc, 'ML-DSA-44'], @@ -86,10 +92,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [false, { name: 'KMAC128', length: 0 }], - [false, { name: 'KMAC256', length: 0 }], - [false, { name: 'KMAC128', length: 1 }], - [false, { name: 'KMAC256', length: 1 }], + [kmac, { name: 'KMAC128', length: 0 }], + [kmac, { name: 'KMAC256', length: 0 }], + [kmac, { name: 'KMAC128', length: 1 }], + [kmac, { name: 'KMAC256', length: 1 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -107,10 +113,10 @@ export const vectors = { [kmac, 'KMAC256'], [kmac, { name: 'KMAC128', length: 256 }], [kmac, { name: 'KMAC256', length: 128 }], - [false, { name: 'KMAC128', length: 0 }], - [false, { name: 'KMAC256', length: 0 }], - [false, { name: 'KMAC128', length: 1 }], - [false, { name: 'KMAC256', length: 1 }], + [kmac, { name: 'KMAC128', length: 0 }], + [kmac, { name: 'KMAC256', length: 0 }], + [kmac, { name: 'KMAC128', length: 1 }], + [kmac, { name: 'KMAC256', length: 1 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -214,7 +220,10 @@ export const vectors = { [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 }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], ], 'decapsulateBits': [ [pqc && !boringSSL, 'ML-KEM-512'], @@ -232,6 +241,9 @@ export const vectors = { [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 }], + [pqc, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 255 }], + [pqc && kmac, 'ML-KEM-768', { name: 'KMAC128', length: 255 }], [false, 'ML-KEM-768', { name: 'HMAC', hash: 'SHA-256', length: 128 }], + [false, 'ML-KEM-768', { name: 'KMAC128', length: 248 }], ], }; diff --git a/test/fixtures/webcrypto/supports-sha3.mjs b/test/fixtures/webcrypto/supports-sha3.mjs index db1aa0e2ac4c0e..73f5b778eeb97f 100644 --- a/test/fixtures/webcrypto/supports-sha3.mjs +++ b/test/fixtures/webcrypto/supports-sha3.mjs @@ -20,12 +20,12 @@ export const vectors = { ], 'generateKey': [ [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], // This interaction is not defined for now. @@ -88,13 +88,13 @@ export const vectors = { 'importKey': [ [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [!boringSSL, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-PSS', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'RSA-OAEP', hash: 'SHA3-256', ...RSA_KEY_GEN }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256' }], [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 256 }], - [false, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], + [!boringSSL, { name: 'HMAC', hash: 'SHA3-256', length: 25 }], [false, { name: 'HMAC', hash: 'SHA3-256', length: 0 }], ], 'get key length': [ 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 66a1a18955fca6..a498df144f3350 100644 --- a/test/parallel/test-crypto-key-objects-to-crypto-key.js +++ b/test/parallel/test-crypto-key-objects-to-crypto-key.js @@ -129,16 +129,26 @@ function genericSecretVectors(name) { ]; } -function macInvalid(algorithm, invalidLengthMessage) { +function macInvalid(algorithm, invalidLengthMessage, allowZeroKey = false) { const key = createSecretKey(randomBytes(32)); const usages = ['sign', 'verify']; - assert.throws(() => { - createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); - }, { - name: 'DataError', - message: 'Zero-length key is not supported', - }); + if (allowZeroKey) { + const zeroKey = createSecretKey(Buffer.alloc(0)) + .toCryptoKey(algorithm, true, usages); + assert.strictEqual(zeroKey.algorithm.length, 0); + + const explicitZeroKey = createSecretKey(Buffer.alloc(0)) + .toCryptoKey({ ...algorithm, length: 0 }, true, usages); + assert.strictEqual(explicitZeroKey.algorithm.length, 0); + } else { + assert.throws(() => { + createSecretKey(Buffer.alloc(0)).toCryptoKey(algorithm, true, usages); + }, { + name: 'DataError', + message: 'Zero-length key is not supported', + }); + } assert.throws(() => key.toCryptoKey(algorithm, true, []), { name: 'SyntaxError', @@ -329,7 +339,7 @@ for (const name of ['KMAC128', 'KMAC256']) { if (name in kSupportedAlgorithms.importKey) { tests[name] = kmacVectors(name); invalid[name] = () => { - macInvalid({ name }, 'KmacImportParams.length cannot be 0'); + macInvalid({ name }, 'Invalid key length', true); }; } } diff --git a/test/parallel/test-webcrypto-derivekey-ecdh.js b/test/parallel/test-webcrypto-derivekey-ecdh.js index 61a284cb8f8dea..1c825b40db7e40 100644 --- a/test/parallel/test-webcrypto-derivekey-ecdh.js +++ b/test/parallel/test-webcrypto-derivekey-ecdh.js @@ -102,6 +102,25 @@ async function prepareKeys() { assert.strictEqual(Buffer.from(raw).toString('hex'), result); } + { + // Non-multiple of 8 derived HMAC key length + const key = await subtle.deriveKey({ + name: 'ECDH', + public: publicKey + }, privateKey, { + name: 'HMAC', + hash: 'SHA-256', + length: 9 + }, true, ['sign', 'verify']); + + const raw = await subtle.exportKey('raw', key); + const expected = Buffer.from(result.slice(0, 4), 'hex'); + expected[1] &= 0b10000000; + + assert.strictEqual(key.algorithm.length, 9); + assert.deepStrictEqual(Buffer.from(raw), expected); + } + { // Case insensitivity const key = await subtle.deriveKey({ diff --git a/test/parallel/test-webcrypto-derivekey.js b/test/parallel/test-webcrypto-derivekey.js index 32c6a93efbbc4e..f9323bca2caf57 100644 --- a/test/parallel/test-webcrypto-derivekey.js +++ b/test/parallel/test-webcrypto-derivekey.js @@ -285,12 +285,19 @@ if (hasOpenSSL(3)) { baseKeyAlgorithm, false, ['deriveKey']); - await assert.rejects( - subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, false, usages), - { - name: 'DataError', - message: /KmacImportParams\.length cannot be 0/, - }); + const derived = await subtle.deriveKey( + algorithm, + baseKey, + derivedKeyAlgorithm, + false, + usages); + assert.strictEqual(derived.algorithm.length, 0); + + const signature = await subtle.sign({ + name: 'KMAC128', + outputLength: 256, + }, derived, new Uint8Array()); + assert.strictEqual(signature.byteLength, 32); } })().then(common.mustCall()); } diff --git a/test/parallel/test-webcrypto-digest.js b/test/parallel/test-webcrypto-digest.js index 4a28d88dcd72c3..8e1b6797ee865b 100644 --- a/test/parallel/test-webcrypto-digest.js +++ b/test/parallel/test-webcrypto-digest.js @@ -9,6 +9,7 @@ const assert = require('assert'); const { Buffer } = require('buffer'); const { subtle } = globalThis.crypto; const { createHash, getHashes } = require('crypto'); +const { hasOpenSSL } = require('../common/crypto'); const kTests = [ ['SHA-1', ['sha1'], 160], @@ -263,9 +264,151 @@ if (getHashes().includes('shake128')) { new Uint8Array(0), ); - await assert.rejects(subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)), { - name: 'NotSupportedError', - message: 'Unsupported CShakeParams outputLength', - }); + const digest = await subtle.digest({ name: 'cSHAKE128', outputLength: 7 }, Buffer.alloc(1)); + assert.strictEqual(digest.byteLength, 1); + assert.strictEqual(new Uint8Array(digest)[0] & 0b00000001, 0); + + await assert.rejects( + subtle.digest( + { name: 'cSHAKE128', outputLength: 0xffffffff }, + Buffer.alloc(1)), + { + name: 'OperationError', + message: 'Invalid CShakeParams outputLength', + }); + + await assert.rejects( + subtle.digest( + { + name: 'cSHAKE128', + outputLength: 256, + functionName: Buffer.from('SHAKE'), + }, + Buffer.alloc(1)), + { + name: 'NotSupportedError', + message: 'Unsupported CShakeParams functionName', + }); + + await assert.rejects( + subtle.digest( + { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.alloc(513), + }, + Buffer.alloc(1)), + { + name: 'OperationError', + message: 'CShakeParams.customization must be at most 512 bytes', + }); + + if (!hasOpenSSL(3)) return; + + const nistCShakeShortInput = Buffer.from('00010203', 'hex'); + const nistCShakeLongInput = + Buffer.from(Array.from({ length: 200 }, (_, i) => i)); + const nistCShakeSample1 = { + algorithm: { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeShortInput, + expected: 'c1c36925b6409a04f1b504fcbca9d82b' + + '4017277cb5ed2b2065fc1d3814d5aaf5', + }; + + for (const { algorithm, data, expected } of [ + nistCShakeSample1, + { + algorithm: { + name: 'cSHAKE128', + outputLength: 256, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeLongInput, + expected: 'c5221d50e4f822d96a2e8881a961420f' + + '294b7b24fe3d2094baed2c6524cc166b', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 512, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeShortInput, + expected: 'd008828e2b80ac9d2218ffee1d070c48' + + 'b8e4c87bff32c9699d5b6896eee0edd1' + + '64020e2be0560858d9c00c037e34a96' + + '937c561a74c412bb4c746469527281c8c', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 512, + customization: Buffer.from('Email Signature'), + }, + data: nistCShakeLongInput, + expected: '07dc27b11e51fbac75bc7b3c1d983e8b' + + '4b85fb1defaf218912ac864302730917' + + '27f42b17ed1df63e8ec118f04b23633c' + + '1dfb1574c8fb55cb45da8e25afb092bb', + }, + { + algorithm: { + name: 'cSHAKE128', + outputLength: 312, + functionName: Buffer.from('KMAC'), + customization: Buffer.from( + '`kiEF`&I))7]yq0?*sKa q)[jP`4R=)lV_9tyvT$kAbH$)1}p].' + + 'bbeomb.'), + }, + data: Buffer.from('ca88f708fa', 'hex'), + expected: 'bebb534ccfccd300f731d2911fb4351d' + + '5fcc95ac2509e9abae8f9dc51106e28d' + + '7f25ae11738334', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 264, + functionName: Buffer.from('TupleHash'), + customization: Buffer.from( + 'q8gN}O&V*VDU4Y.^5J13tG2,1^Lw~C2rw $AB3.SX)=@z'), + }, + data: Buffer.from('13d101da', 'hex'), + expected: '43163a57fc1ee8f1c501a2add927698c' + + 'a5a4b52c0d3ef3fd6d91d8d2386765e0ae', + }, + { + algorithm: { + name: 'cSHAKE256', + outputLength: 312, + functionName: Buffer.from('ParallelHash'), + customization: Buffer.from( + 'vD-1>T,f.R*V%ZA[ OyJ'), + }, + data: Buffer.from('d5d7e7517f', 'hex'), + expected: '442be69b2afd7c8282839920a8446aaf' + + '16a5049d3d018eac87e04cf9225870ef' + + 'ca6f88db415829', + }, + ]) { + assert.strictEqual( + Buffer.from(await subtle.digest(algorithm, data)).toString('hex'), + expected); + } + + const truncated = Buffer.from(await subtle.digest( + { ...nistCShakeSample1.algorithm, outputLength: 255 }, + nistCShakeSample1.data)); + const expected = Buffer.from(nistCShakeSample1.expected, 'hex'); + assert.strictEqual(truncated.byteLength, expected.byteLength); + assert.deepStrictEqual(truncated.subarray(0, 31), expected.subarray(0, 31)); + assert.strictEqual(truncated[31] & 0b00000001, 0); + assert.strictEqual(truncated[31] | 0b00000001, expected[31]); })().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 f3850dcdf02546..e31be79e6bfe4b 100644 --- a/test/parallel/test-webcrypto-encap-decap-ml-kem.js +++ b/test/parallel/test-webcrypto-encap-decap-ml-kem.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals 'use strict'; const common = require('../common'); @@ -12,11 +13,16 @@ if (!hasOpenSSL(3, 5) && !process.features.openssl_is_boringssl) const assert = require('assert'); const crypto = require('crypto'); +const { getCryptoKeyHandle } = require('internal/crypto/keys'); const { KeyObject } = crypto; const { subtle } = globalThis.crypto; const vectors = require('../fixtures/crypto/ml-kem')(); +function getCryptoKeyData(key) { + return getCryptoKeyHandle(key).export(); +} + async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results }) { const [ publicKey, @@ -65,6 +71,19 @@ async function testEncapsulateKey({ name, publicKeyPem, privateKeyPem, results } assert.strictEqual(encapsulated2.sharedKey.algorithm.name, 'HMAC'); assert.strictEqual(encapsulated2.sharedKey.extractable, false); + const encapsulated3 = await subtle.encapsulateKey( + { name }, + publicKey, + { name: 'HMAC', hash: 'SHA-256', length: 255 }, + false, + ['sign', 'verify'] + ); + + assert.strictEqual(encapsulated3.sharedKey.algorithm.name, 'HMAC'); + assert.strictEqual(encapsulated3.sharedKey.algorithm.length, 255); + assert.strictEqual(getCryptoKeyData(encapsulated3.sharedKey).length, 32); + assert.strictEqual(getCryptoKeyData(encapsulated3.sharedKey)[31] & 0b00000001, 0); + // Test failure when using wrong key type await assert.rejects( subtle.encapsulateKey({ name }, privateKey, 'HKDF', false, ['deriveBits']), { @@ -159,6 +178,19 @@ async function testDecapsulateKey({ name, publicKeyPem, privateKeyPem, results } const decapsulatedKeyData = KeyObject.from(decapsulatedKey).export(); assert(originalKeyData.equals(decapsulatedKeyData)); + const decapsulatedHmac = await subtle.decapsulateKey( + { name }, + privateKey, + encapsulated.ciphertext, + { name: 'HMAC', hash: 'SHA-256', length: 255 }, + false, + ['sign', 'verify'] + ); + assert.strictEqual(decapsulatedHmac.algorithm.name, 'HMAC'); + assert.strictEqual(decapsulatedHmac.algorithm.length, 255); + assert.strictEqual(getCryptoKeyData(decapsulatedHmac).length, 32); + assert.strictEqual(getCryptoKeyData(decapsulatedHmac)[31] & 0b00000001, 0); + // Test with test vector ciphertext and expected shared key const vectorDecapsulatedKey = await subtle.decapsulateKey( { name }, diff --git a/test/parallel/test-webcrypto-export-import.js b/test/parallel/test-webcrypto-export-import.js index 6c8f36a62eccb3..c7399c69d9c93f 100644 --- a/test/parallel/test-webcrypto-export-import.js +++ b/test/parallel/test-webcrypto-export-import.js @@ -72,8 +72,8 @@ const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto') hash: 'SHA-256', length: 1 }, false, ['sign', 'verify']), { - name: 'NotSupportedError', - message: 'Unsupported HmacImportParams.length' + name: 'DataError', + message: 'Invalid key length' }); await assert.rejects( subtle.importKey('jwk', null, { @@ -88,6 +88,55 @@ const { createPrivateKey, createPublicKey, createSecretKey } = require('crypto') test().then(common.mustCall()); } +// HMAC non-byte key lengths +{ + async function test() { + const generated = await subtle.generateKey( + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + + const importedExplicit = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + const importedImplicit = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { name: 'HMAC', hash: 'SHA-256' }, + true, + ['sign', 'verify']); + const importedImplicitRaw = await subtle.exportKey('raw', importedImplicit); + assert.strictEqual(importedImplicit.algorithm.length, 16); + assert.deepStrictEqual( + new Uint8Array(importedImplicitRaw), + new Uint8Array([0xff, 0xff])); + + await assert.rejects( + subtle.importKey( + 'raw', + new Uint8Array([0xff]), + { name: 'HMAC', hash: 'SHA-256', length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } + + test().then(common.mustCall()); +} + // Import/Export HMAC Secret Key { async function test() { @@ -230,6 +279,69 @@ if (hasOpenSSL(3)) { true, [/* empty usages */]), { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' }); + + { + const importedZeroImplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + name, + true, + ['sign', 'verify']); + const importedZeroImplicitRaw = + await subtle.exportKey('raw-secret', importedZeroImplicit); + assert.strictEqual(importedZeroImplicit.algorithm.length, 0); + assert.strictEqual(importedZeroImplicitRaw.byteLength, 0); + + const importedZeroExplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array(), + { name, length: 0 }, + true, + ['sign', 'verify']); + const importedZeroExplicitRaw = + await subtle.exportKey('raw-secret', importedZeroExplicit); + assert.strictEqual(importedZeroExplicit.algorithm.length, 0); + assert.strictEqual(importedZeroExplicitRaw.byteLength, 0); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 0 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + + const generated = await subtle.generateKey( + { name, length: 9 }, + true, + ['sign', 'verify']); + const generatedRaw = await subtle.exportKey('raw-secret', generated); + assert.strictEqual(generated.algorithm.length, 9); + assert.strictEqual(generatedRaw.byteLength, 2); + assert.strictEqual(new Uint8Array(generatedRaw)[1] & 0b01111111, 0); + + const importedExplicit = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']); + const importedExplicitRaw = await subtle.exportKey('raw-secret', importedExplicit); + assert.strictEqual(importedExplicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(importedExplicitRaw), + new Uint8Array([0xff, 0x80])); + + await assert.rejects( + subtle.importKey( + 'raw-secret', + new Uint8Array([0xff]), + { name, length: 9 }, + true, + ['sign', 'verify']), + { name: 'DataError', message: 'Invalid key length' }); + } } test('KMAC128').then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-keygen-kmac.js b/test/parallel/test-webcrypto-keygen-kmac.js index 3061f19c826ff5..c1125412892e16 100644 --- a/test/parallel/test-webcrypto-keygen-kmac.js +++ b/test/parallel/test-webcrypto-keygen-kmac.js @@ -17,7 +17,7 @@ const { subtle } = globalThis.crypto; const usages = ['sign', 'verify']; async function test(name, length) { - length = length ?? name === 'KMAC128' ? 128 : 256; + length ??= name === 'KMAC128' ? 128 : 256; const key = await subtle.generateKey({ name, length, @@ -34,12 +34,17 @@ async function test(name, length) { assert.strictEqual(key.algorithm.length, length); assert.strictEqual(key.algorithm, key.algorithm); assert.strictEqual(key.usages, key.usages); + + const raw = await subtle.exportKey('raw-secret', key); + assert.strictEqual(raw.byteLength, Math.ceil(length / 8)); } const kTests = [ + ['KMAC128', 0], ['KMAC128', 128], ['KMAC128', 256], ['KMAC128'], + ['KMAC256', 0], ['KMAC256', 128], ['KMAC256', 256], ['KMAC256'], diff --git a/test/parallel/test-webcrypto-sign-verify-kmac.js b/test/parallel/test-webcrypto-sign-verify-kmac.js index 008047630753b2..f93fc293b2a4f7 100644 --- a/test/parallel/test-webcrypto-sign-verify-kmac.js +++ b/test/parallel/test-webcrypto-sign-verify-kmac.js @@ -17,10 +17,14 @@ const vectors = require('../fixtures/crypto/kmac')(); async function testVerify({ algorithm, key, + keyLength, data, customization, outputLength, expected }) { + const importAlgorithm = keyLength === undefined ? + { name: algorithm } : + { name: algorithm, length: keyLength }; const [ verifyKey, noVerifyKey, @@ -29,13 +33,13 @@ async function testVerify({ algorithm, subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify']), subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['sign']), subtle.generateKey( @@ -119,10 +123,14 @@ async function testVerify({ algorithm, async function testSign({ algorithm, key, + keyLength, data, customization, outputLength, expected }) { + const importAlgorithm = keyLength === undefined ? + { name: algorithm } : + { name: algorithm, length: keyLength }; const [ signKey, noSignKey, @@ -131,13 +139,13 @@ async function testSign({ algorithm, subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify', 'sign']), subtle.importKey( 'raw-secret', key, - { name: algorithm }, + importAlgorithm, false, ['verify']), subtle.generateKey( @@ -191,3 +199,75 @@ async function testSign({ algorithm, await Promise.all(variations); })().then(common.mustCall()); + +(async function() { + const key = await subtle.importKey( + 'raw-secret', + new Uint8Array(16), + { name: 'KMAC128' }, + false, + ['sign', 'verify']); + const algorithm = { + name: 'KMAC128', + outputLength: 9, + customization: new Uint8Array(), + }; + const data = new Uint8Array([1, 2, 3]); + + const signature = await subtle.sign(algorithm, key, data); + assert.strictEqual(signature.byteLength, 2); + assert.strictEqual(new Uint8Array(signature)[1] & 0b01111111, 0); + assert(await subtle.verify(algorithm, key, signature, data)); + + const signature16 = new Uint8Array(await subtle.sign({ + ...algorithm, + outputLength: 16, + }, key, data)); + signature16[1] &= 0b10000000; + assert.notDeepStrictEqual(new Uint8Array(signature), signature16); + + const invalidSignature = new Uint8Array(signature); + invalidSignature[1] |= 0b00000001; + assert(!(await subtle.verify(algorithm, key, invalidSignature, data))); + + const nonByteKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + { name: 'KMAC128', length: 25 }, + false, + ['sign', 'verify']); + const nonByteKeySignature = await subtle.sign({ + ...algorithm, + outputLength: 16, + }, nonByteKey, data); + assert.strictEqual(nonByteKeySignature.byteLength, 2); + assert(await subtle.verify({ + ...algorithm, + outputLength: 16, + }, nonByteKey, nonByteKeySignature, data)); +})().then(common.mustCall()); + +(async function() { + const data = new Uint8Array([1, 2, 3]); + + for (const name of ['KMAC128', 'KMAC256']) { + for (const keyData of [ + new Uint8Array(), + new Uint8Array([1]), + new Uint8Array([1, 2, 3]), + ]) { + const key = await subtle.importKey( + 'raw-secret', + keyData, + { name }, + true, + ['sign', 'verify']); + assert.strictEqual(key.algorithm.length, keyData.byteLength * 8); + + const algorithm = { name, outputLength: 256 }; + const signature = await subtle.sign(algorithm, key, data); + assert.strictEqual(signature.byteLength, 32); + assert(await subtle.verify(algorithm, key, signature, data)); + } + } +})().then(common.mustCall()); diff --git a/test/parallel/test-webcrypto-supports.mjs b/test/parallel/test-webcrypto-supports.mjs index c3c976f730e065..43d3ee03c8bc29 100644 --- a/test/parallel/test-webcrypto-supports.mjs +++ b/test/parallel/test-webcrypto-supports.mjs @@ -61,13 +61,13 @@ function supportsRawSecret(alg) { return false; } -function supports256RawSecret(alg) { +function supportsEncapsulatedRawSecret(alg) { if (!supportsRawSecret(alg)) return false; switch (alg?.name?.toLowerCase?.()) { case 'hmac': case 'kmac128': case 'kmac256': - return typeof alg.length !== 'number' || alg.length === 256; + return typeof alg.length !== 'number' || Math.ceil(alg.length / 8) === 32; default: return true; } @@ -75,7 +75,7 @@ function supports256RawSecret(alg) { for (const encap of vectors.encapsulateBits) { for (const imp of vectors.importKey) { - if (supports256RawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(imp[1])) { vectors.encapsulateKey.push([encap[0] && imp[0], encap[1], imp[1]]); } else { vectors.encapsulateKey.push([false, encap[1], imp[1]]); @@ -85,7 +85,7 @@ for (const encap of vectors.encapsulateBits) { for (const decap of vectors.decapsulateBits) { for (const imp of vectors.importKey) { - if (supports256RawSecret(imp[1])) { + if (supportsEncapsulatedRawSecret(imp[1])) { vectors.decapsulateKey.push([decap[0] && imp[0], decap[1], imp[1]]); } else { vectors.decapsulateKey.push([false, decap[1], imp[1]]); diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 49f63e215fadfc..18ba6dc3796378 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -369,6 +369,89 @@ function testWrapping(name, keys) { await Promise.all(variations); })().then(common.mustCall()); +async function testNonByteLengthWrapUnwrap({ + key, + formats, + rawFormat, + explicitAlgorithm, + implicitAlgorithm, +}) { + const wrappingKey = await subtle.generateKey( + { name: 'AES-GCM', length: 128 }, + true, + ['wrapKey', 'unwrapKey']); + const expectedRaw = new Uint8Array(await subtle.exportKey(rawFormat, key)); + + for (const [i, format] of formats.entries()) { + const wrapAlgorithm = { + name: 'AES-GCM', + iv: new Uint8Array(12).fill(i), + }; + const wrapped = await subtle.wrapKey(format, key, wrappingKey, wrapAlgorithm); + + // The serialized key material carries bytes, not the requested bit length. + const explicit = await subtle.unwrapKey( + format, + wrapped, + wrappingKey, + wrapAlgorithm, + explicitAlgorithm, + true, + ['sign', 'verify']); + assert.strictEqual(explicit.algorithm.length, 9); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey(rawFormat, explicit)), + expectedRaw); + + const implicit = await subtle.unwrapKey( + format, + wrapped, + wrappingKey, + wrapAlgorithm, + implicitAlgorithm, + true, + ['sign', 'verify']); + assert.strictEqual(implicit.algorithm.length, expectedRaw.byteLength * 8); + assert.deepStrictEqual( + new Uint8Array(await subtle.exportKey(rawFormat, implicit)), + expectedRaw); + } +} + +(async function() { + const hmacAlgorithm = { name: 'HMAC', hash: 'SHA-256' }; + const hmacKey = await subtle.importKey( + 'raw', + new Uint8Array([0xff, 0xff]), + { ...hmacAlgorithm, length: 9 }, + true, + ['sign', 'verify']); + await testNonByteLengthWrapUnwrap({ + key: hmacKey, + formats: ['raw', 'jwk'], + rawFormat: 'raw', + explicitAlgorithm: { ...hmacAlgorithm, length: 9 }, + implicitAlgorithm: hmacAlgorithm, + }); + + if (hasOpenSSL(3)) { + const kmacAlgorithm = { name: 'KMAC128' }; + const kmacKey = await subtle.importKey( + 'raw-secret', + new Uint8Array([0xff, 0xff]), + { ...kmacAlgorithm, length: 9 }, + true, + ['sign', 'verify']); + await testNonByteLengthWrapUnwrap({ + key: kmacKey, + formats: ['raw-secret', 'jwk'], + rawFormat: 'raw-secret', + explicitAlgorithm: { ...kmacAlgorithm, length: 9 }, + implicitAlgorithm: kmacAlgorithm, + }); + } +})().then(common.mustCall()); + // Test that wrapKey/unwrapKey validate the wrapping/unwrapping key's // algorithm and usage before proceeding. // Spec: https://w3c.github.io/webcrypto/#SubtleCrypto-method-wrapKey From b05f59a6b4b60176a3d4019c0e5dc4e51645c1b0 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 20 Jun 2026 14:37:58 +0200 Subject: [PATCH 8/9] crypto: reject small-order EdDSA points during verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return false for Ed25519 and Ed448 one-shot verification when the public key or signature R component is a known low-order point. This keeps key import behavior unchanged while making WebCrypto verification match WPT expectations across OpenSSL variants. Remove the stale WPT expected-failure entry and add focused regression coverage for both curves. Closes: https://github.com/nodejs/node/issues/54572 Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/64026 Fixes: https://github.com/nodejs/node/issues/54572 Reviewed-By: Yagiz Nizipli Reviewed-By: Tobias Nießen --- src/crypto/crypto_sig.cc | 144 +++++++++++++++++- .../test-webcrypto-sign-verify-eddsa.js | 41 +++++ test/wpt/status/WebCryptoAPI.cjs | 19 --- 3 files changed, 184 insertions(+), 20 deletions(-) diff --git a/src/crypto/crypto_sig.cc b/src/crypto/crypto_sig.cc index 851b715c67b495..eab8a30b4f15ef 100644 --- a/src/crypto/crypto_sig.cc +++ b/src/crypto/crypto_sig.cc @@ -78,6 +78,147 @@ bool ApplyRSAOptions(const EVPKeyPointer& pkey, return true; } +constexpr size_t kEd25519PointSize = 32; +constexpr size_t kEd448PointSize = 57; + +// Ed25519 has cofactor 8, so the first eight entries are the full +// canonical small-order subgroup: identity, one point of order 2, +// two points of order 4, and four points of order 8. +constexpr unsigned char kEd25519SmallOrderPoints[][kEd25519PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 8. + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a}, + {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, + 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, + 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05}, + {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, + 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, + 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85}, + // Non-canonical encodings of the same small-order points. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, + {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, + {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, +}; + +// Ed448 has cofactor 4, so these four entries are the full canonical +// small-order subgroup: identity, one point of order 2, and two points +// of order 4. +constexpr unsigned char kEd448SmallOrderPoints[][kEd448PointSize] = { + // Identity. + {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + // Order 2. + {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00}, + // Order 4. + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80}, +}; + +template +bool ContainsPoint(const unsigned char* candidate, + const unsigned char (&points)[Count][PointSize]) { + for (const auto& point : points) { + if (memcmp(candidate, point, PointSize) == 0) return true; + } + return false; +} + +bool IsSmallOrderEdDsaPoint(int id, + const unsigned char* candidate, + size_t size) { + switch (id) { + case EVP_PKEY_ED25519: + return size == kEd25519PointSize && + ContainsPoint(candidate, kEd25519SmallOrderPoints); + case EVP_PKEY_ED448: + return size == kEd448PointSize && + ContainsPoint(candidate, kEd448SmallOrderPoints); + default: + return false; + } +} + +bool HasSmallOrderEdDsaPoint(const EVPKeyPointer& key, + const ByteSource& signature) { + const int id = key.id(); + size_t point_size; + + switch (id) { + case EVP_PKEY_ED25519: + point_size = kEd25519PointSize; + break; + case EVP_PKEY_ED448: + point_size = kEd448PointSize; + break; + default: + return false; + } + + if (signature.size() != point_size * 2) return false; + + if (IsSmallOrderEdDsaPoint(id, signature.data(), point_size)) { + return true; + } + + unsigned char raw_public_key[kEd448PointSize]; + size_t raw_public_key_size = point_size; + if (EVP_PKEY_get_raw_public_key( + key.get(), raw_public_key, &raw_public_key_size) != 1) { + return false; + } + + return IsSmallOrderEdDsaPoint(id, raw_public_key, raw_public_key_size); +} + std::unique_ptr Node_SignFinal(Environment* env, EVPMDCtxPointer&& mdctx, const EVPKeyPointer& pkey, @@ -757,7 +898,8 @@ bool SignTraits::DeriveBits(Environment* env, case SignConfiguration::Mode::Verify: { auto buf = DataPointer::Alloc(1); static_cast(buf.get())[0] = 0; - if (context.verify(params.data, params.signature)) { + if (context.verify(params.data, params.signature) && + !HasSmallOrderEdDsaPoint(key, params.signature)) { static_cast(buf.get())[0] = 1; } *out = ByteSource::Allocated(buf.release()); diff --git a/test/parallel/test-webcrypto-sign-verify-eddsa.js b/test/parallel/test-webcrypto-sign-verify-eddsa.js index 1dfd3ddd76a6a0..3c40139754bea9 100644 --- a/test/parallel/test-webcrypto-sign-verify-eddsa.js +++ b/test/parallel/test-webcrypto-sign-verify-eddsa.js @@ -15,6 +15,44 @@ const vectors = require('../fixtures/crypto/eddsa')(); const supportsContext = hasOpenSSL(3, 2); +const smallOrderVerifyVectors = [ + { + name: 'Ed25519', + publicKey: Buffer.from( + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa', + 'hex'), + signature: Buffer.from( + 'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a' + + '0000000000000000000000000000000000000000000000000000000000000000', + 'hex'), + data: Buffer.from( + '8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6', + 'hex'), + }, +]; + +if (!process.features.openssl_is_boringssl) { + smallOrderVerifyVectors.push({ + name: 'Ed448', + publicKey: Buffer.concat([Buffer.from([1]), Buffer.alloc(56)]), + signature: Buffer.concat([Buffer.from([1]), Buffer.alloc(113)]), + data: Buffer.from([1, 2, 3]), + }); +} + +async function testSmallOrderVerify({ name, publicKey, signature, data }) { + const key = await subtle.importKey( + 'raw', + publicKey, + { name }, + false, + ['verify']); + + assert.strictEqual( + await subtle.verify({ name }, key, signature, data), + false); +} + async function testVerify({ name, context, publicKeyBuffer, @@ -260,6 +298,9 @@ async function testSign({ name, variations.push(testVerify(vector)); variations.push(testSign(vector)); }); + smallOrderVerifyVectors.forEach((vector) => { + variations.push(testSmallOrderVerify(vector)); + }); await Promise.all(variations); })().then(common.mustCall()); diff --git a/test/wpt/status/WebCryptoAPI.cjs b/test/wpt/status/WebCryptoAPI.cjs index c9e32046c3cf69..8fdbb458fac7e3 100644 --- a/test/wpt/status/WebCryptoAPI.cjs +++ b/test/wpt/status/WebCryptoAPI.cjs @@ -1,11 +1,7 @@ 'use strict'; -const os = require('node:os'); - const { hasOpenSSL } = require('../../common/crypto.js'); -const s390x = os.arch() === 's390x'; - const conditionalFileSkips = {}; const conditionalSubtestSkips = {}; @@ -115,21 +111,6 @@ module.exports = { 'historical.any.js': { 'skip': 'Not relevant in Node.js context', }, - 'sign_verify/eddsa_small_order_points.https.any.js': { - 'fail': { - 'note': 'see https://github.com/nodejs/node/issues/54572', - 'expected': [ - 'Ed25519 Verification checks with small-order key of order - Test 1', - 'Ed25519 Verification checks with small-order key of order - Test 2', - 'Ed25519 Verification checks with small-order key of order - Test 12', - 'Ed25519 Verification checks with small-order key of order - Test 13', - ...(s390x ? [] : [ - 'Ed25519 Verification checks with small-order key of order - Test 0', - 'Ed25519 Verification checks with small-order key of order - Test 11', - ]), - ], - }, - }, 'getRandomValues.any.js': { 'fail': { 'note': 'https://github.com/nodejs/node/issues/58987', From 1526f768b1e63de379bfde6c676caffaac70a5d8 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Sat, 20 Jun 2026 18:54:02 +0200 Subject: [PATCH 9/9] lib: fix missing lazyDOMException import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/64033 Reviewed-By: Antoine du Hamel Reviewed-By: René --- lib/internal/crypto/hash.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index 10224a4cd89fc2..b51388418d2ee1 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -38,6 +38,7 @@ const { } = require('internal/crypto/keys'); const { + lazyDOMException, normalizeEncoding, encodingsMap, isPendingDeprecation,