From 9cea4fd0d90fd999897aed19e7c1b4308eba1f76 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 30 Mar 2026 12:46:32 +0200 Subject: [PATCH] crypto: accept key data in crypto.diffieHellman() and cleanup DH jobs Signed-off-by: Filip Skokan --- doc/api/crypto.md | 15 +- lib/internal/crypto/diffiehellman.js | 79 ++++++++--- lib/internal/crypto/keys.js | 28 ++-- lib/internal/crypto/sig.js | 4 +- src/crypto/crypto_dh.cc | 20 ++- src/crypto/crypto_ec.cc | 103 -------------- src/crypto/crypto_ec.h | 35 ----- .../test-crypto-dh-stateless-async.js | 129 ++++++++++++++++++ test/parallel/test-crypto-dh-stateless.js | 121 ++++++++++++++++ 9 files changed, 347 insertions(+), 187 deletions(-) diff --git a/doc/api/crypto.md b/doc/api/crypto.md index fb80671b8bd8a6..5c541199157a9d 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -4142,23 +4142,32 @@ added: - v13.9.0 - v12.17.0 changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/62527 + description: Accept key data in addition to KeyObject instances. - version: v23.11.0 pr-url: https://github.com/nodejs/node/pull/57274 description: Optional callback argument added. --> * `options` {Object} - * `privateKey` {KeyObject} - * `publicKey` {KeyObject} + * `privateKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} + * `publicKey` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|KeyObject} * `callback` {Function} * `err` {Error} * `secret` {Buffer} * Returns: {Buffer} if the `callback` function is not provided. Computes the Diffie-Hellman shared secret based on a `privateKey` and a `publicKey`. -Both keys must have the same `asymmetricKeyType` and must support either the DH or +Both keys must represent the same asymmetric key type and must support either the DH or ECDH operation. +If `options.privateKey` is not a [`KeyObject`][], this function behaves as if +`options.privateKey` had been passed to [`crypto.createPrivateKey()`][]. + +If `options.publicKey` is not a [`KeyObject`][], this function behaves as if +`options.publicKey` had been passed to [`crypto.createPublicKey()`][]. + If the `callback` function is provided this function uses libuv's threadpool. ### `crypto.encapsulate(key[, callback])` diff --git a/lib/internal/crypto/diffiehellman.js b/lib/internal/crypto/diffiehellman.js index 58e2bc0c91ec3c..8e631ecf7053f5 100644 --- a/lib/internal/crypto/diffiehellman.js +++ b/lib/internal/crypto/diffiehellman.js @@ -17,7 +17,6 @@ const { DiffieHellman: _DiffieHellman, DiffieHellmanGroup: _DiffieHellmanGroup, ECDH: _ECDH, - ECDHBitsJob, ECDHConvertKey: _ECDHConvertKey, kCryptoJobAsync, kCryptoJobSync, @@ -52,9 +51,11 @@ const { } = require('internal/util'); const { - KeyObject, + isKeyObject, kAlgorithm, kKeyType, + preparePrivateKey, + preparePublicOrPrivateKey, } = require('internal/crypto/keys'); const { @@ -284,31 +285,65 @@ function diffieHellman(options, callback) { validateFunction(callback, 'callback'); const { privateKey, publicKey } = options; - if (!(privateKey instanceof KeyObject)) + + // TODO(@panva): remove these non-semver-major error code preserving measures + // in a semver-major followup, the final state is just preparePublicOrPrivateKey + // and preparePrivateKey + if (privateKey == null) throw new ERR_INVALID_ARG_VALUE('options.privateKey', privateKey); - if (!(publicKey instanceof KeyObject)) + if (publicKey == null) throw new ERR_INVALID_ARG_VALUE('options.publicKey', publicKey); - if (privateKey.type !== 'private') - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private'); + if (isKeyObject(privateKey)) { + if (privateKey.type !== 'private') + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(privateKey.type, 'private'); + } - if (publicKey.type !== 'public' && publicKey.type !== 'private') { - throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type, - 'private or public'); + if (isKeyObject(publicKey)) { + if (publicKey.type !== 'public' && publicKey.type !== 'private') { + throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(publicKey.type, + 'private or public'); + } } - const privateType = privateKey.asymmetricKeyType; - const publicType = publicKey.asymmetricKeyType; - if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { - throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', - `${privateType} and ${publicType}`); + if (isKeyObject(privateKey) && isKeyObject(publicKey)) { + const privateType = privateKey.asymmetricKeyType; + const publicType = publicKey.asymmetricKeyType; + if (privateType !== publicType || !dhEnabledKeyTypes.has(privateType)) { + throw new ERR_CRYPTO_INCOMPATIBLE_KEY('key types for Diffie-Hellman', + `${privateType} and ${publicType}`); + } } + const { + data: pubData, + format: pubFormat, + type: pubType, + passphrase: pubPassphrase, + namedCurve: pubNamedCurve, + } = preparePublicOrPrivateKey(publicKey, 'options.publicKey'); + + const { + data: privData, + format: privFormat, + type: privType, + passphrase: privPassphrase, + namedCurve: privNamedCurve, + } = preparePrivateKey(privateKey, 'options.privateKey'); + const job = new DHBitsJob( callback ? kCryptoJobAsync : kCryptoJobSync, - publicKey[kHandle], - privateKey[kHandle]); + pubData, + pubFormat, + pubType, + pubPassphrase, + pubNamedCurve, + privData, + privFormat, + privType, + privPassphrase, + privNamedCurve); if (!callback) { const { 0: err, 1: secret } = job.run(); @@ -349,10 +384,18 @@ async function ecdhDeriveBits(algorithm, baseKey, length) { throw lazyDOMException('Named curve mismatch', 'InvalidAccessError'); } - const bits = await jobPromise(() => new ECDHBitsJob( + const bits = await jobPromise(() => new DHBitsJob( kCryptoJobAsync, key[kKeyObject][kHandle], - baseKey[kKeyObject][kHandle])); + undefined, + undefined, + undefined, + undefined, + baseKey[kKeyObject][kHandle], + undefined, + undefined, + undefined, + undefined)); // If a length is not specified, return the full derived secret if (length === null) diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index d66f03a4ebcea7..4c91443d5d4951 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -628,7 +628,7 @@ function getKeyTypes(allowKeyObject, bufferOnly = false) { } -function prepareAsymmetricKey(key, ctx) { +function prepareAsymmetricKey(key, ctx, name = 'key') { if (isKeyObject(key)) { // Best case: A key object, as simple as that. return { data: getKeyObjectHandle(key, ctx) }; @@ -639,7 +639,7 @@ function prepareAsymmetricKey(key, ctx) { } if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. - return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, 'key') }; + return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, name) }; } if (typeof key === 'object') { const { key: data, encoding, format } = key; @@ -654,23 +654,23 @@ function prepareAsymmetricKey(key, ctx) { return { data: getKeyObjectHandle(data[kKeyObject], ctx) }; } if (format === 'jwk') { - validateObject(data, 'key.key'); + validateObject(data, `${name}.key`); return { data, format: kKeyFormatJWK }; } else if (format === 'raw-public' || format === 'raw-private' || format === 'raw-seed') { if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( - 'key.key', + `${name}.key`, ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], data); } - validateString(key.asymmetricKeyType, 'key.asymmetricKeyType'); + validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`); if (key.asymmetricKeyType === 'ec') { - validateString(key.namedCurve, 'key.namedCurve'); + validateString(key.namedCurve, `${name}.namedCurve`); } const rawFormat = parseKeyFormat(format, undefined, 'options.format'); return { - data: getArrayBufferOrView(data, 'key.key'), + data: getArrayBufferOrView(data, `${name}.key`), format: rawFormat, type: key.asymmetricKeyType, namedCurve: key.namedCurve ?? null, @@ -680,7 +680,7 @@ function prepareAsymmetricKey(key, ctx) { // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( - 'key.key', + `${name}.key`, getKeyTypes(ctx !== kCreatePrivate), data); } @@ -688,23 +688,23 @@ function prepareAsymmetricKey(key, ctx) { const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; return { - data: getArrayBufferOrView(data, 'key', encoding), + data: getArrayBufferOrView(data, `${name}.key`, encoding), ...parseKeyEncoding(key, undefined, isPublic), }; } throw new ERR_INVALID_ARG_TYPE( - 'key', + name, getKeyTypes(ctx !== kCreatePrivate), key); } -function preparePrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePrivate); +function preparePrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePrivate, name); } -function preparePublicOrPrivateKey(key) { - return prepareAsymmetricKey(key, kConsumePublic); +function preparePublicOrPrivateKey(key, name) { + return prepareAsymmetricKey(key, kConsumePublic, name); } function prepareSecretKey(key, encoding, bufferOnly = false) { diff --git a/lib/internal/crypto/sig.js b/lib/internal/crypto/sig.js index 324b804a817a3d..ec8c889b20a8c6 100644 --- a/lib/internal/crypto/sig.js +++ b/lib/internal/crypto/sig.js @@ -135,7 +135,7 @@ Sign.prototype.sign = function sign(options, encoding) { throw new ERR_CRYPTO_SIGN_KEY_REQUIRED(); const { data, format, type, passphrase, namedCurve } = - preparePrivateKey(options, true); + preparePrivateKey(options); // Options specific to RSA const rsaPadding = getPadding(options); @@ -239,7 +239,7 @@ Verify.prototype.verify = function verify(options, signature, sigEncoding) { type, passphrase, namedCurve, - } = preparePublicOrPrivateKey(options, true); + } = preparePublicOrPrivateKey(options); // Options specific to RSA const rsaPadding = getPadding(options); diff --git a/src/crypto/crypto_dh.cc b/src/crypto/crypto_dh.cc index 0a14ebc9cdcd80..36e38e99f06fe0 100644 --- a/src/crypto/crypto_dh.cc +++ b/src/crypto/crypto_dh.cc @@ -477,20 +477,16 @@ Maybe DHBitsTraits::AdditionalConfig( const FunctionCallbackInfo& args, unsigned int offset, DHBitsConfig* params) { - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key + auto public_key = KeyObjectData::GetPublicOrPrivateKeyFromJs(args, &offset); + if (!public_key) [[unlikely]] + return Nothing(); - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; + auto private_key = KeyObjectData::GetPrivateKeyFromJs(args, &offset, true); + if (!private_key) [[unlikely]] + return Nothing(); - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); - - CHECK(private_key->Data().GetKeyType() == kKeyTypePrivate); - CHECK(public_key->Data().GetKeyType() != kKeyTypeSecret); - - params->public_key = public_key->Data().addRef(); - params->private_key = private_key->Data().addRef(); + params->public_key = std::move(public_key); + params->private_key = std::move(private_key); return JustVoid(); } diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 6738edd590c300..9cd50a421f8715 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -39,7 +39,6 @@ using v8::JustVoid; using v8::Local; using v8::LocalVector; using v8::Maybe; -using v8::MaybeLocal; using v8::Nothing; using v8::Object; using v8::String; @@ -68,7 +67,6 @@ void ECDH::Initialize(Environment* env, Local target) { SetMethodNoSideEffect(context, target, "ECDHConvertKey", ECDH::ConvertKey); SetMethodNoSideEffect(context, target, "getCurves", ECDH::GetCurves); - ECDHBitsJob::Initialize(env, target); ECKeyPairGenJob::Initialize(env, target); NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE); @@ -86,7 +84,6 @@ void ECDH::RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(ECDH::ConvertKey); registry->Register(ECDH::GetCurves); - ECDHBitsJob::RegisterExternalReferences(registry); ECKeyPairGenJob::RegisterExternalReferences(registry); } @@ -391,106 +388,6 @@ void ECDH::ConvertKey(const FunctionCallbackInfo& args) { } } -void ECDHBitsConfig::MemoryInfo(MemoryTracker* tracker) const { - tracker->TrackField("public", public_); - tracker->TrackField("private", private_); -} - -MaybeLocal ECDHBitsTraits::EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out) { - return out->ToArrayBuffer(env); -} - -Maybe ECDHBitsTraits::AdditionalConfig( - CryptoJobMode mode, - const FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params) { - Environment* env = Environment::GetCurrent(args); - - CHECK(args[offset]->IsObject()); // public key - CHECK(args[offset + 1]->IsObject()); // private key - - KeyObjectHandle* private_key; - KeyObjectHandle* public_key; - - ASSIGN_OR_RETURN_UNWRAP(&public_key, args[offset], Nothing()); - ASSIGN_OR_RETURN_UNWRAP(&private_key, args[offset + 1], Nothing()); - - if (private_key->Data().GetKeyType() != kKeyTypePrivate || - public_key->Data().GetKeyType() != kKeyTypePublic) { - THROW_ERR_CRYPTO_INVALID_KEYTYPE(env); - return Nothing(); - } - - params->private_ = private_key->Data().addRef(); - params->public_ = public_key->Data().addRef(); - - return JustVoid(); -} - -bool ECDHBitsTraits::DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out, - CryptoJobMode mode, - CryptoErrorStore* errors) { - size_t len = 0; - const auto& m_privkey = params.private_.GetAsymmetricKey(); - const auto& m_pubkey = params.public_.GetAsymmetricKey(); - - switch (m_privkey.id()) { - case EVP_PKEY_X25519: - // Fall through - case EVP_PKEY_X448: { - Mutex::ScopedLock pub_lock(params.public_.mutex()); - EVPKeyCtxPointer ctx = m_privkey.newCtx(); - if (!ctx.initForDerive(m_pubkey)) return false; - - auto data = ctx.derive(); - if (!data) return false; - DCHECK(!data.isSecure()); - - *out = ByteSource::Allocated(data.release()); - break; - } - default: { - const EC_KEY* private_key; - { - Mutex::ScopedLock priv_lock(params.private_.mutex()); - private_key = m_privkey; - } - - Mutex::ScopedLock pub_lock(params.public_.mutex()); - const EC_KEY* public_key = m_pubkey; - - const auto group = ECKeyPointer::GetGroup(private_key); - if (group == nullptr) { - errors->Insert(NodeCryptoError::ECDH_FAILED); - return false; - } - - CHECK(ECKeyPointer::Check(private_key)); - CHECK(ECKeyPointer::Check(public_key)); - const auto pub = ECKeyPointer::GetPublicKey(public_key); - int field_size = EC_GROUP_get_degree(group); - len = (field_size + 7) / 8; - auto buf = DataPointer::Alloc(len); - CHECK_NOT_NULL(pub); - CHECK_NOT_NULL(private_key); - if (ECDH_compute_key( - static_cast(buf.get()), len, pub, private_key, nullptr) <= - 0) { - return false; - } - - *out = ByteSource::Allocated(buf.release()); - } - } - - return true; -} - EVPKeyCtxPointer EcKeyGenTraits::Setup(EcKeyPairGenConfig* params) { EVPKeyCtxPointer key_ctx; switch (params->params.curve_nid) { diff --git a/src/crypto/crypto_ec.h b/src/crypto/crypto_ec.h index 5522ac743e3089..9bc817f3d7f5d3 100644 --- a/src/crypto/crypto_ec.h +++ b/src/crypto/crypto_ec.h @@ -55,41 +55,6 @@ class ECDH final : public BaseObject { const EC_GROUP* group_; }; -struct ECDHBitsConfig final : public MemoryRetainer { - int id_; - KeyObjectData private_; - KeyObjectData public_; - - void MemoryInfo(MemoryTracker* tracker) const override; - SET_MEMORY_INFO_NAME(ECDHBitsConfig) - SET_SELF_SIZE(ECDHBitsConfig) -}; - -struct ECDHBitsTraits final { - using AdditionalParameters = ECDHBitsConfig; - static constexpr const char* JobName = "ECDHBitsJob"; - static constexpr AsyncWrap::ProviderType Provider = - AsyncWrap::PROVIDER_DERIVEBITSREQUEST; - - static v8::Maybe AdditionalConfig( - CryptoJobMode mode, - const v8::FunctionCallbackInfo& args, - unsigned int offset, - ECDHBitsConfig* params); - - static bool DeriveBits(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out_, - CryptoJobMode mode, - CryptoErrorStore* errors); - - static v8::MaybeLocal EncodeOutput(Environment* env, - const ECDHBitsConfig& params, - ByteSource* out); -}; - -using ECDHBitsJob = DeriveBitsJob; - struct EcKeyPairParams final : public MemoryRetainer { int curve_nid; int param_encoding; diff --git a/test/parallel/test-crypto-dh-stateless-async.js b/test/parallel/test-crypto-dh-stateless-async.js index 891c9a983e0603..ac3aeed3b23aee 100644 --- a/test/parallel/test-crypto-dh-stateless-async.js +++ b/test/parallel/test-crypto-dh-stateless-async.js @@ -221,3 +221,132 @@ test(crypto.generateKeyPairSync('x25519'), assert.strictEqual(err.code, hasOpenSSL3 ? 'ERR_OSSL_FAILED_DURING_DERIVATION' : 'ERR_CRYPTO_OPERATION_FAILED'); })); } + +// Test all key encoding formats +for (const { privateKey: alicePriv, publicKey: bobPub } of [ + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), + crypto.generateKeyPairSync('x25519'), +]) { + const expected = crypto.diffieHellman({ + privateKey: alicePriv, + publicKey: bobPub, + }); + + const encodings = [ + // PEM string + { + privateKey: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + publicKey: bobPub.export({ type: 'spki', format: 'pem' }), + }, + // PEM { key, format } object + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + format: 'pem', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'pem' }), + format: 'pem', + }, + }, + // DER PKCS#8 / SPKI + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'der' }), + format: 'der', + type: 'pkcs8', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'der' }), + format: 'der', + type: 'spki', + }, + }, + // JWK + { + privateKey: { key: alicePriv.export({ format: 'jwk' }), format: 'jwk' }, + publicKey: { key: bobPub.export({ format: 'jwk' }), format: 'jwk' }, + }, + // Raw key material + { + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: alicePriv.asymmetricKeyType, + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType: bobPub.asymmetricKeyType, + ...bobPub.asymmetricKeyDetails, + }, + }, + ]; + + // EC-only encodings + if (alicePriv.asymmetricKeyType === 'ec') { + // DER SEC1 private key + encodings.push({ + privateKey: { + key: alicePriv.export({ type: 'sec1', format: 'der' }), + format: 'der', + type: 'sec1', + }, + publicKey: bobPub, + }); + // Raw with compressed public key + encodings.push({ + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: 'ec', + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public', type: 'compressed' }), + format: 'raw-public', + asymmetricKeyType: 'ec', + ...bobPub.asymmetricKeyDetails, + }, + }); + } + + for (const options of encodings) { + crypto.diffieHellman(options, common.mustSucceed((buf) => { + assert.deepStrictEqual(buf, expected); + })); + } +} + +// Test C++ error conditions (delivered via the callback) +{ + const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); + const x25519 = crypto.generateKeyPairSync('x25519'); + const ed25519 = crypto.generateKeyPairSync('ed25519'); + + // Mismatching EC curves + crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ec384.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS'); + })); + + // Incompatible key types (ec + x25519) + crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: x25519.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE'); + })); + + // Unsupported key type (ed25519) + crypto.diffieHellman({ + privateKey: ed25519.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ed25519.publicKey.export({ type: 'spki', format: 'pem' }), + }, common.mustCall((err) => { + assert.strictEqual(err.code, 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE'); + })); +} diff --git a/test/parallel/test-crypto-dh-stateless.js b/test/parallel/test-crypto-dh-stateless.js index 84ebbd21aad6a1..3941a263dbef4c 100644 --- a/test/parallel/test-crypto-dh-stateless.js +++ b/test/parallel/test-crypto-dh-stateless.js @@ -302,3 +302,124 @@ assert.throws(() => { { name: 'Error', message: /Deriving bits failed/ }, ); } + +// Test all key encoding formats +for (const { privateKey: alicePriv, publicKey: bobPub } of [ + crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }), + crypto.generateKeyPairSync('x25519'), +]) { + const expected = crypto.diffieHellman({ + privateKey: alicePriv, + publicKey: bobPub, + }); + + const encodings = [ + // PEM string + { + privateKey: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + publicKey: bobPub.export({ type: 'spki', format: 'pem' }), + }, + // PEM { key, format } object + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'pem' }), + format: 'pem', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'pem' }), + format: 'pem', + }, + }, + // DER PKCS#8 / SPKI + { + privateKey: { + key: alicePriv.export({ type: 'pkcs8', format: 'der' }), + format: 'der', + type: 'pkcs8', + }, + publicKey: { + key: bobPub.export({ type: 'spki', format: 'der' }), + format: 'der', + type: 'spki', + }, + }, + // JWK + { + privateKey: { key: alicePriv.export({ format: 'jwk' }), format: 'jwk' }, + publicKey: { key: bobPub.export({ format: 'jwk' }), format: 'jwk' }, + }, + // Raw key material + { + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: alicePriv.asymmetricKeyType, + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public' }), + format: 'raw-public', + asymmetricKeyType: bobPub.asymmetricKeyType, + ...bobPub.asymmetricKeyDetails, + }, + }, + ]; + + // EC-only encodings + if (alicePriv.asymmetricKeyType === 'ec') { + // DER SEC1 private key + encodings.push({ + privateKey: { + key: alicePriv.export({ type: 'sec1', format: 'der' }), + format: 'der', + type: 'sec1', + }, + publicKey: bobPub, + }); + // Raw with compressed public key + encodings.push({ + privateKey: { + key: alicePriv.export({ format: 'raw-private' }), + format: 'raw-private', + asymmetricKeyType: 'ec', + ...alicePriv.asymmetricKeyDetails, + }, + publicKey: { + key: bobPub.export({ format: 'raw-public', type: 'compressed' }), + format: 'raw-public', + asymmetricKeyType: 'ec', + ...bobPub.asymmetricKeyDetails, + }, + }); + } + + for (const options of encodings) { + assert.deepStrictEqual(crypto.diffieHellman(options), expected); + } +} + +// Test C++ error conditions +{ + const ec256 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-256' }); + const ec384 = crypto.generateKeyPairSync('ec', { namedCurve: 'P-384' }); + const x25519 = crypto.generateKeyPairSync('x25519'); + const ed25519 = crypto.generateKeyPairSync('ed25519'); + + // Mismatching EC curves + assert.throws(() => crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ec384.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_MISMATCHING_DOMAIN_PARAMETERS' }); + + // Incompatible key types (ec + x25519) + assert.throws(() => crypto.diffieHellman({ + privateKey: ec256.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: x25519.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); + + // Unsupported key type (ed25519) + assert.throws(() => crypto.diffieHellman({ + privateKey: ed25519.privateKey.export({ type: 'pkcs8', format: 'pem' }), + publicKey: ed25519.publicKey.export({ type: 'spki', format: 'pem' }), + }), { code: 'ERR_OSSL_EVP_OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE' }); +}