From ca3bfce230e1574e89ff4212bf40701fffe0cfba Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 28 May 2026 15:37:10 +0200 Subject: [PATCH] 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 --- 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 3e6152b1f55501..681926a4b904c9 100644 --- a/lib/internal/crypto/cfrg.js +++ b/lib/internal/crypto/cfrg.js @@ -11,6 +11,7 @@ const { kCryptoJobWebCrypto, kKeyFormatDER, kKeyFormatRawPublic, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatPKCS8, @@ -37,8 +38,6 @@ const { const { getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -176,12 +175,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 e2497a2b722b97..9eeabaee223112 100644 --- a/lib/internal/crypto/ml_dsa.js +++ b/lib/internal/crypto/ml_dsa.js @@ -13,6 +13,7 @@ const { kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, + kKeyTypePublic, kSignJobModeSign, kSignJobModeVerify, kWebCryptoKeyFormatRaw, @@ -38,8 +39,6 @@ const { const { getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -142,12 +141,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 2dea4d00af052f..f6df9239b21545 100644 --- a/lib/internal/crypto/ml_kem.js +++ b/lib/internal/crypto/ml_kem.js @@ -14,6 +14,7 @@ const { kKeyFormatDER, kKeyFormatRawPublic, kKeyFormatRawSeed, + kKeyTypePublic, kWebCryptoKeyFormatPKCS8, kWebCryptoKeyFormatRaw, kWebCryptoKeyFormatSPKI, @@ -37,8 +38,6 @@ const { const { getCryptoKeyHandle, getCryptoKeyType, - getKeyObjectHandle, - getKeyObjectType, InternalCryptoKey, } = require('internal/crypto/keys'); @@ -143,12 +142,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 05c337d3262229..e4700da3959324 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -43,7 +43,6 @@ const { } = require('internal/errors'); const { - createPublicKey, CryptoKey, getCryptoKeyAlgorithm, getCryptoKeyExtractable, @@ -53,7 +52,7 @@ const { getCryptoKeyUsagesMask, hasCryptoKeyUsage, importGenericSecretKey, - PrivateKeyObject, + toPublicCryptoKey, } = require('internal/crypto/keys'); const { @@ -1469,10 +1468,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 c1b7aee576519e..40b5ae9563ee7b 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -1021,6 +1021,7 @@ Local KeyObjectHandle::Initialize(Environment* env) { KeyObjectHandle::kInternalFieldCount); SetProtoMethod(isolate, templ, "init", Init); + SetProtoMethodNoSideEffect(isolate, templ, "getKeyType", GetKeyType); SetProtoMethodNoSideEffect( isolate, templ, "getSymmetricKeySize", GetSymmetricKeySize); SetProtoMethodNoSideEffect( @@ -1048,6 +1049,7 @@ void KeyObjectHandle::RegisterExternalReferences( ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Init); + registry->Register(GetKeyType); registry->Register(GetSymmetricKeySize); registry->Register(GetAsymmetricKeyType); registry->Register(CheckEcKeyData); @@ -1178,6 +1180,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 6adedc89fafffe..fd3b0b0d0fb7a7 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' + });