From e9c84d6898e9ca6da75480fb0fea02ee30a5a86d Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 7 Sep 2026 23:42:13 +0200 Subject: [PATCH 1/6] crypto: optimize private EC JWK import Avoid creating temporary EVP objects and repeating key validation while retaining the private scalar range and public/private consistency checks. Assisted-by: GitHub Copilot Signed-off-by: Filip Skokan --- deps/ncrypto/ncrypto.cc | 18 +++++ deps/ncrypto/ncrypto.h | 1 + src/crypto/crypto_ec.cc | 7 +- .../test-crypto-key-objects-ec-jwk-private.js | 79 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-crypto-key-objects-ec-jwk-private.js diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 20f22e614525..acc4ec0bb712 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -5348,6 +5348,24 @@ bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) { // ============================================================================ +bool ECKeyPointer::checkPrivateKey() const { + const auto group = getGroup(); + const auto priv = getPrivateKey(); + const auto pub = getPublicKey(); + if (group == nullptr || priv == nullptr || pub == nullptr) return false; + + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(group, order.get(), nullptr) || + BN_is_zero(priv) || BN_is_negative(priv) || + BN_cmp(priv, order.get()) >= 0) { + return false; + } + + auto expected = ECPointPointer::New(group); + return expected && expected.mul(group, priv) && + EC_POINT_cmp(group, expected.get(), pub, nullptr) == 0; +} + #if NCRYPTO_USE_LEGACY_KEY_TYPES ECKeyPointer::ECKeyPointer() : key_(nullptr) {} diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 6b1edceed061..633c2c4202a3 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -1764,6 +1764,7 @@ class ECKeyPointer final { bool setPublicKeyRaw(const BignumPointer& x, const BignumPointer& y); bool generate(); bool checkKey() const; + bool checkPrivateKey() const; DataPointer computeSecret(const ECPointPointer& peer) const; const EC_GROUP* getGroup() const; diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index d6920c8b30e2..d9b88e264a7f 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -756,7 +756,7 @@ KeyObjectData ImportJWKEcKey(Environment* env, Local jwk) { return {}; } // Verify that the public point matches the private scalar (d*G == (x,y)). - if (!ec.checkKey()) { + if (!ec.checkPrivateKey()) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); return {}; } @@ -764,7 +764,10 @@ KeyObjectData ImportJWKEcKey(Environment* env, Local jwk) { auto pkey = EVPKeyPointer::New(); if (!pkey) return {}; - CHECK(pkey.set(ec)); + if (!pkey.set(ec)) { + THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); + return {}; + } return KeyObjectData::CreateAsymmetric(type, std::move(pkey)); } diff --git a/test/parallel/test-crypto-key-objects-ec-jwk-private.js b/test/parallel/test-crypto-key-objects-ec-jwk-private.js new file mode 100644 index 000000000000..bede4e2f31c0 --- /dev/null +++ b/test/parallel/test-crypto-key-objects-ec-jwk-private.js @@ -0,0 +1,79 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createECDH, + createPrivateKey, + createPublicKey, + getCurves, + getFips, + sign, + verify, +} = require('crypto'); + +const curves = [ + ['prime256v1', 'P-256', 32, + 'ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'], + ['secp384r1', 'P-384', 48, + 'ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf' + + '581a0db248b0a77aecec196accc52973'], + ['secp521r1', 'P-521', 66, + '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + + 'fa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'], +]; +if (!getFips() && getCurves().includes('secp256k1')) { + curves.push(['secp256k1', 'secp256k1', 32, + 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141']); +} + +for (const [namedCurve, crv, width, orderHex] of curves) { + const order = BigInt(`0x${orderHex}`); + const encode = (scalar) => Buffer.from( + scalar.toString(16).padStart(width * 2, '0'), 'hex'); + const makeJwk = (scalar) => { + const ecdh = createECDH(namedCurve); + ecdh.setPrivateKey(encode(scalar)); + const point = ecdh.getPublicKey(); + return { + kty: 'EC', + crv, + x: point.subarray(1, 1 + width).toString('base64url'), + y: point.subarray(1 + width).toString('base64url'), + d: encode(scalar).toString('base64url'), + }; + }; + const generator = makeJwk(1n); + const other = makeJwk(2n); + const message = Buffer.from('EC JWK private key consistency'); + + for (const jwk of [generator, other, makeJwk(order - 1n)]) { + const key = createPrivateKey({ format: 'jwk', key: jwk }); + assert.deepStrictEqual(key.export({ format: 'jwk' }), jwk); + const publicJwk = { kty: jwk.kty, crv, x: jwk.x, y: jwk.y }; + const publicKey = createPublicKey({ format: 'jwk', key: publicJwk }); + assert(verify('sha256', message, publicKey, sign('sha256', message, key))); + } + + const invalid = [ + { ...generator, d: other.d }, + { ...generator, x: other.x, y: other.y }, + ...[0n, order, order + 1n].map((scalar) => ({ + ...generator, d: encode(scalar).toString('base64url'), + })), + { ...generator, d: '' }, + { + ...generator, + x: Buffer.alloc(width).toString('base64url'), + y: Buffer.alloc(width).toString('base64url'), + }, + ]; + for (const jwk of invalid) { + assert.throws(() => createPrivateKey({ format: 'jwk', key: jwk }), { + code: 'ERR_CRYPTO_INVALID_JWK', + }); + } +} From faa428871110d9cd4a8228d8f59d4eae801c5052 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 8 Sep 2026 00:20:36 +0200 Subject: [PATCH 2/6] crypto: read EC curve metadata directly Avoid reconstructing EC keys for key details and TLS ephemeral-key curve reporting. Assisted-by: GitHub Copilot Signed-off-by: Filip Skokan --- benchmark/crypto/ec-key-details.js | 23 +++++++++++++++++++++++ deps/ncrypto/ncrypto.cc | 18 ++++++++++++++++++ deps/ncrypto/ncrypto.h | 1 + src/crypto/crypto_common.cc | 5 +---- src/crypto/crypto_ec.cc | 6 +----- 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 benchmark/crypto/ec-key-details.js diff --git a/benchmark/crypto/ec-key-details.js b/benchmark/crypto/ec-key-details.js new file mode 100644 index 000000000000..e7266e9ad19b --- /dev/null +++ b/benchmark/crypto/ec-key-details.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common.js'); +const { KeyObject } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['P-256', 'P-384', 'P-521'], + type: ['public', 'private'], + n: [10000], +}); + +async function main({ namedCurve, type, n }) { + const pair = await crypto.subtle.generateKey({ + name: 'ECDSA', namedCurve, + }, true, ['sign', 'verify']); + const cryptoKey = pair[`${type}Key`]; + bench.start(); + for (let index = 0; index < n; index++) { + if (!KeyObject.from(cryptoKey).asymmetricKeyDetails.namedCurve) + throw new Error('Missing named curve'); + } + bench.end(n); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index acc4ec0bb712..6309febf5a17 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -6933,6 +6933,24 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +int Ec::GetCurveId(const EVPKeyPointer& key) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + char name[80]; + size_t length = 0; + if (EVP_PKEY_get_utf8_string_param( + key.get(), OSSL_PKEY_PARAM_GROUP_NAME, name, sizeof(name), &length) != + 1) { + return NID_undef; + } + return GetCurveIdFromName(name); +#else + const EC_KEY* ec = key; + if (ec == nullptr) return NID_undef; + const EC_GROUP* group = EC_KEY_get0_group(ec); + return group == nullptr ? NID_undef : EC_GROUP_get_curve_name(group); +#endif +} + int Ec::GetCurveIdFromName(const char* name) { int nid = EC_curve_nist2nid(name); if (nid == NID_undef) { diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 633c2c4202a3..798d8a6130ca 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -812,6 +812,7 @@ class Ec final { #endif static int GetCurveIdFromName(const char* name); + static int GetCurveId(const EVPKeyPointer& key); using GetCurveCallback = std::function; static bool GetCurves(GetCurveCallback callback); diff --git a/src/crypto/crypto_common.cc b/src/crypto/crypto_common.cc index fde12953860c..b1b1c48e4cc7 100644 --- a/src/crypto/crypto_common.cc +++ b/src/crypto/crypto_common.cc @@ -28,7 +28,6 @@ namespace node { using ncrypto::ClearErrorOnReturn; -using ncrypto::ECKeyPointer; using ncrypto::EVPKeyPointer; using ncrypto::SSLPointer; using ncrypto::SSLSessionPointer; @@ -231,9 +230,7 @@ MaybeLocal GetEphemeralKey(Environment* env, const SSLPointer& ssl) { case EVP_PKEY_X448: { const char* curve_name; if (kid == EVP_PKEY_EC) { - ECKeyPointer ec(key); - if (!ec) break; - int nid = EC_GROUP_get_curve_name(ec.getGroup()); + int nid = ncrypto::Ec::GetCurveId(key); if (nid == NID_undef) break; curve_name = OBJ_nid2sn(nid); } else { diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index d9b88e264a7f..17f7999f22bd 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -779,11 +779,7 @@ bool GetEcKeyDetail(Environment* env, const auto& m_pkey = key.GetAsymmetricKey(); CHECK_EQ(m_pkey.id(), EVP_PKEY_EC); - ECKeyPointer ec(m_pkey); - if (!ec) return true; - - const auto group = ec.getGroup(); - int nid = EC_GROUP_get_curve_name(group); + int nid = Ec::GetCurveId(m_pkey); if (nid == NID_undef) return true; return target From ad911242d7bb684d07c4d876e9dc91ba3322e602 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 8 Sep 2026 00:27:41 +0200 Subject: [PATCH 3/6] crypto: export EC JWK coordinates directly Query provider coordinates together instead of serializing and decoding the public point. Assisted-by: GitHub Copilot Signed-off-by: Filip Skokan --- benchmark/crypto/ec-jwk-export.js | 19 ++++++++++ deps/ncrypto/ncrypto.cc | 59 +++++++++++++++++++++++++++++++ deps/ncrypto/ncrypto.h | 5 +++ src/crypto/crypto_ec.cc | 35 +++++++----------- 4 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 benchmark/crypto/ec-jwk-export.js diff --git a/benchmark/crypto/ec-jwk-export.js b/benchmark/crypto/ec-jwk-export.js new file mode 100644 index 000000000000..0539fc6468a0 --- /dev/null +++ b/benchmark/crypto/ec-jwk-export.js @@ -0,0 +1,19 @@ +'use strict'; + +const common = require('../common.js'); +const { generateKeyPairSync } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['prime256v1', 'secp384r1', 'secp521r1', 'secp256k1'], + type: ['public', 'private'], + n: [10000], +}); + +function main({ namedCurve, type, n }) { + const key = generateKeyPairSync('ec', { namedCurve })[`${type}Key`]; + const options = { format: 'jwk' }; + bench.start(); + for (let index = 0; index < n; index++) + key.export(options); + bench.end(n); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 6309febf5a17..f7b2898d94a5 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -6933,6 +6933,65 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +bool Ec::GetKeyComponents(const EVPKeyPointer& key, + BignumPointer* x, + BignumPointer* y, + BignumPointer* priv, + int* degree) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + const int nid = GetCurveId(key); + switch (nid) { + case NID_X9_62_prime256v1: + case NID_secp256k1: + *degree = 256; + break; + case NID_secp384r1: + *degree = 384; + break; + case NID_secp521r1: + *degree = 521; + break; + default: + *degree = 0; + } + if (*degree != 0) { + MarkPopErrorOnReturn pop_errors; + unsigned char x_bytes[66]{}; + unsigned char y_bytes[66]{}; + const size_t width = (*degree + 7) / 8; + OSSL_PARAM params[] = { + OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_X, x_bytes, width), + OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_Y, y_bytes, width), + OSSL_PARAM_construct_end(), + }; + if (EVP_PKEY_get_params(key.get(), params) == 1 && + OSSL_PARAM_modified(¶ms[0]) && OSSL_PARAM_modified(¶ms[1])) { + x->reset(BN_native2bn(x_bytes, width, nullptr)); + y->reset(BN_native2bn(y_bytes, width, nullptr)); + return *x && *y && + (priv == nullptr || + GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv)); + } + } +#endif + ECKeyPointer ec(key); + if (!ec || ec.getPublicKey() == nullptr) return false; + *degree = EC_GROUP_get_degree(ec.getGroup()); + x->reset(BN_new()); + y->reset(BN_new()); + if (!*x || !*y || + EC_POINT_get_affine_coordinates( + ec.getGroup(), ec.getPublicKey(), x->get(), y->get(), nullptr) != 1) { + return false; + } + if (priv != nullptr) { + if (ec.getPrivateKey() == nullptr) return false; + priv->reset(BN_dup(ec.getPrivateKey())); + if (!*priv) return false; + } + return true; +} + int Ec::GetCurveId(const EVPKeyPointer& key) { #if NCRYPTO_USE_OPENSSL3_PROVIDER char name[80]; diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 798d8a6130ca..397d68c731cb 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -813,6 +813,11 @@ class Ec final { static int GetCurveIdFromName(const char* name); static int GetCurveId(const EVPKeyPointer& key); + static bool GetKeyComponents(const EVPKeyPointer& key, + BignumPointer* x, + BignumPointer* y, + BignumPointer* priv, + int* degree); using GetCurveCallback = std::function; static bool GetCurves(GetCurveCallback callback); diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 17f7999f22bd..19f0487dd664 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -488,30 +488,22 @@ bool ExportJWKEcKey(Environment* env, const auto& m_pkey = key.GetAsymmetricKey(); CHECK_EQ(m_pkey.id(), EVP_PKEY_EC); - ECKeyPointer ec(m_pkey); - if (!ec) { + BignumPointer x; + BignumPointer y; + BignumPointer priv; + int degree_bits; + if (!Ec::GetKeyComponents( + m_pkey, + &x, + &y, + key.GetKeyType() == kKeyTypePrivate ? &priv : nullptr, + °ree_bits)) { THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); return false; } - // A provider-backed key need not expose its public point. - if (ec.getPublicKey() == nullptr) return false; - - const auto pub = ec.getPublicKey(); - const auto group = ec.getGroup(); - - int degree_bits = EC_GROUP_get_degree(group); int degree_bytes = (degree_bits / CHAR_BIT) + (7 + (degree_bits % CHAR_BIT)) / 8; - auto x = BignumPointer::New(); - auto y = BignumPointer::New(); - - if (!EC_POINT_get_affine_coordinates(group, pub, x.get(), y.get(), nullptr)) { - ThrowCryptoError(env, ERR_get_error(), - "Failed to get elliptic-curve point coordinates"); - return false; - } - if (!target ->DefineOwnProperty( env->context(), env->jwk_kty_string(), env->jwk_ec_string()) @@ -535,7 +527,7 @@ bool ExportJWKEcKey(Environment* env, } Local crv_name; - const int nid = EC_GROUP_get_curve_name(group); + const int nid = Ec::GetCurveId(m_pkey); switch (nid) { case NID_X9_62_prime256v1: crv_name = env->p256_string(); @@ -562,9 +554,8 @@ bool ExportJWKEcKey(Environment* env, } if (key.GetKeyType() == kKeyTypePrivate) { - auto pvt = ec.getPrivateKey(); - if (pvt == nullptr) return false; - return SetEncodedValue(env, target, env->jwk_d_string(), pvt, degree_bytes) + return SetEncodedValue( + env, target, env->jwk_d_string(), priv.get(), degree_bytes) .IsJust(); } From 842794da7400b9b3c760b270b69565c8bce9da4f Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 8 Sep 2026 00:34:22 +0200 Subject: [PATCH 4/6] crypto: avoid EC raw export reconstruction Read private scalars and matching uncompressed provider encodings directly. Assisted-by: GitHub Copilot Signed-off-by: Filip Skokan --- benchmark/crypto/ec-raw-export.js | 23 ++++++++++ deps/ncrypto/ncrypto.cc | 45 ++++++++++++++++++ deps/ncrypto/ncrypto.h | 3 ++ src/crypto/crypto_keys.cc | 76 ++++++++++--------------------- 4 files changed, 95 insertions(+), 52 deletions(-) create mode 100644 benchmark/crypto/ec-raw-export.js diff --git a/benchmark/crypto/ec-raw-export.js b/benchmark/crypto/ec-raw-export.js new file mode 100644 index 000000000000..15c1d9333bb4 --- /dev/null +++ b/benchmark/crypto/ec-raw-export.js @@ -0,0 +1,23 @@ +'use strict'; + +const common = require('../common.js'); +const { generateKeyPairSync } = require('crypto'); + +const bench = common.createBenchmark(main, { + namedCurve: ['prime256v1', 'secp384r1', 'secp521r1'], + format: ['raw-private', 'raw-public'], + type: ['uncompressed', 'compressed'], + n: [10000], +}, { + combinationFilter: ({ format, type }) => format === 'raw-public' || type === 'uncompressed', +}); + +function main({ namedCurve, format, type, n }) { + const pair = generateKeyPairSync('ec', { namedCurve }); + const key = format === 'raw-private' ? pair.privateKey : pair.publicKey; + const options = format === 'raw-public' ? { format, type } : { format }; + bench.start(); + for (let index = 0; index < n; index++) + key.export(options); + bench.end(n); +} diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index f7b2898d94a5..9cf724c1560c 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -6933,6 +6933,51 @@ int Ec::getCurve() const { return EC_GROUP_get_curve_name(getGroup()); } +DataPointer Ec::TryExportPublic(const EVPKeyPointer& key, + point_conversion_form_t form) { + if (form != POINT_CONVERSION_UNCOMPRESSED) return {}; +#if NCRYPTO_USE_OPENSSL3_PROVIDER + { + MarkPopErrorOnReturn pop_errors; + size_t length = 0; + if (EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &length) == 1) { + auto bytes = DataPointer::Alloc(length); + if (bytes && length != 0 && + EVP_PKEY_get_octet_string_param(key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + bytes.get(), + length, + &length) == 1 && + (bytes.get()[0] & ~1) == form) { + return bytes.resize(length); + } + } + } +#endif + return {}; +} + +DataPointer Ec::ExportPrivate(const EVPKeyPointer& key) { +#if NCRYPTO_USE_OPENSSL3_PROVIDER + { + MarkPopErrorOnReturn pop_errors; + BignumPointer priv; + BignumPointer order; + if (GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv) && + GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_EC_ORDER, &order)) { + return priv.encodePadded(order.byteLength()); + } + } +#endif + ECKeyPointer ec(key); + if (!ec || ec.getPrivateKey() == nullptr) return {}; + auto order = BignumPointer::New(); + if (!order || !EC_GROUP_get_order(ec.getGroup(), order.get(), nullptr)) + return {}; + return BignumPointer::EncodePadded(ec.getPrivateKey(), order.byteLength()); +} + bool Ec::GetKeyComponents(const EVPKeyPointer& key, BignumPointer* x, BignumPointer* y, diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 397d68c731cb..944c42490d6c 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -813,6 +813,9 @@ class Ec final { static int GetCurveIdFromName(const char* name); static int GetCurveId(const EVPKeyPointer& key); + static DataPointer TryExportPublic(const EVPKeyPointer& key, + point_conversion_form_t form); + static DataPointer ExportPrivate(const EVPKeyPointer& key); static bool GetKeyComponents(const EVPKeyPointer& key, BignumPointer* x, BignumPointer* y, diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc index 49f3128f1fa0..b7d3ef9ae6da 100644 --- a/src/crypto/crypto_keys.cc +++ b/src/crypto/crypto_keys.cc @@ -386,21 +386,24 @@ bool KeyObjectData::ToEncodedPublicKey( Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); if (pkey.id() == EVP_PKEY_EC) { + auto form = static_cast(config.ec_point_form); + auto bytes = ncrypto::Ec::TryExportPublic(pkey, form); + if (bytes) + return Buffer::Copy(env, bytes.get(), bytes.size()) + .ToLocal(out); ECKeyPointer ec_key(pkey); if (!ec_key) { THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); return false; } - // A provider-backed key need not expose its public point. if (ec_key.getPublicKey() == nullptr) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to export EC public key"); return false; } - auto form = static_cast(config.ec_point_form); - const auto group = ec_key.getGroup(); - const auto point = ec_key.getPublicKey(); - return ECPointToBuffer(env, group, point, form).ToLocal(out); + return ECPointToBuffer( + env, ec_key.getGroup(), ec_key.getPublicKey(), form) + .ToLocal(out); } const int id = pkey.id(); bool is_raw_supported = id == EVP_PKEY_ED25519 || id == EVP_PKEY_ED448 || @@ -441,25 +444,7 @@ bool KeyObjectData::ToEncodedPrivateKey( Mutex::ScopedLock lock(mutex()); const auto& pkey = GetAsymmetricKey(); if (pkey.id() == EVP_PKEY_EC) { - ECKeyPointer ec_key(pkey); - if (!ec_key) { - THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - return false; - } - const BIGNUM* private_key = ec_key.getPrivateKey(); - if (private_key == nullptr) { - THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "Failed to export EC private key"); - return false; - } - const auto group = ec_key.getGroup(); - auto order = BignumPointer::New(); - if (!order || !EC_GROUP_get_order(group, order.get(), nullptr)) { - THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "Failed to export EC private key"); - return false; - } - auto buf = BignumPointer::EncodePadded(private_key, order.byteLength()); + auto buf = ncrypto::Ec::ExportPrivate(pkey); if (!buf) { THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to export EC private key"); @@ -1581,24 +1566,27 @@ void KeyObjectHandle::ExportECPublicRaw( return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } + CHECK(args[0]->IsInt32()); + auto form = + static_cast(args[0].As()->Value()); + + auto bytes = ncrypto::Ec::TryExportPublic(m_pkey, form); + if (bytes) { + args.GetReturnValue().Set( + Buffer::Copy(env, bytes.get(), bytes.size()) + .FromMaybe(Local())); + return; + } ECKeyPointer ec_key(m_pkey); if (!ec_key) return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - // A provider-backed key need not expose its public point. if (ec_key.getPublicKey() == nullptr) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to export EC public key"); } - - CHECK(args[0]->IsInt32()); - auto form = - static_cast(args[0].As()->Value()); - - const auto group = ec_key.getGroup(); - const auto point = ec_key.getPublicKey(); - Local buf; - if (!ECPointToBuffer(env, group, point, form).ToLocal(&buf)) return; - + if (!ECPointToBuffer(env, ec_key.getGroup(), ec_key.getPublicKey(), form) + .ToLocal(&buf)) + return; args.GetReturnValue().Set(buf); } @@ -1617,23 +1605,7 @@ void KeyObjectHandle::ExportECPrivateRaw( return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); } - ECKeyPointer ec_key(m_pkey); - if (!ec_key) return THROW_ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(env); - - const BIGNUM* private_key = ec_key.getPrivateKey(); - if (private_key == nullptr) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "Failed to export EC private key"); - } - - const auto group = ec_key.getGroup(); - auto order = BignumPointer::New(); - if (!order || !EC_GROUP_get_order(group, order.get(), nullptr)) { - return THROW_ERR_CRYPTO_OPERATION_FAILED(env, - "Failed to export EC private key"); - } - - auto buf = BignumPointer::EncodePadded(private_key, order.byteLength()); + auto buf = ncrypto::Ec::ExportPrivate(m_pkey); if (!buf) { return THROW_ERR_CRYPTO_OPERATION_FAILED(env, "Failed to export EC private key"); From b406b68f0a226baf2cc7125f2e749175c5669e58 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Mon, 7 Sep 2026 22:36:38 +0200 Subject: [PATCH 5/6] crypto: avoid EC reconstruction for signature sizing Use EVP_PKEY_bits() to determine the width of ECDSA signature components on OpenSSL 3. This avoids reconstructing the EC group and public point just to read the group order size. Signed-off-by: Filip Skokan Assisted-by: GitHub Copilot --- deps/ncrypto/ncrypto.cc | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 9cf724c1560c..9c88d919e9ad 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -4130,11 +4130,7 @@ std::optional EVPKeyPointer::getBytesOfRS() const { #endif } else if (id == EVP_PKEY_EC) { #if NCRYPTO_USE_OPENSSL3_PROVIDER - Ec ec(get()); - if (!ec) return std::nullopt; - const EC_GROUP* group = ec.getGroup(); - if (group == nullptr) return std::nullopt; - bits = EC_GROUP_order_bits(group); + bits = EVP_PKEY_bits(get()); #else const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get()); if (ec_key == nullptr) return std::nullopt; From f349ae17720759b38a5dc776a002163b3ac3d7aa Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 8 Sep 2026 13:56:50 +0200 Subject: [PATCH 6/6] fixup! crypto: export EC JWK coordinates directly --- src/crypto/crypto_ec.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/crypto/crypto_ec.cc b/src/crypto/crypto_ec.cc index 19f0487dd664..f9263635d0b0 100644 --- a/src/crypto/crypto_ec.cc +++ b/src/crypto/crypto_ec.cc @@ -498,7 +498,6 @@ bool ExportJWKEcKey(Environment* env, &y, key.GetKeyType() == kKeyTypePrivate ? &priv : nullptr, °ree_bits)) { - THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key"); return false; } int degree_bytes =