Commit 642f172de06 for nodejs
commit 642f172de06ae07c1b622cd7dab7d12926a76de9
Author: Filip Skokan <panva.ip@gmail.com>
Date: Tue Sep 22 22:36:59 2026 +0200
crypto: include EC public keys in PKCS8 exports
Export Web Crypto EC private keys from a clone with the public point
included, even when the imported encoding omitted it. Preserve the
original KeyObject encoding state.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66237
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Aviv Keller <me@aviv.sh>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index fe99e84d7fa..52f841422c7 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -7384,6 +7384,24 @@ int Ec::getCurve() const {
return EC_GROUP_get_curve_name(getGroup());
}
+BIOPointer Ec::ExportPrivatePkcs8(const EVPKeyPointer& key) {
+ MarkPopErrorOnReturn mark_pop_error_on_return;
+ if (!key || !key.isA(KeyAlgorithm::EC)) return {};
+ auto ec = ECKeyPointer(key).clone();
+ if (!ec) return {};
+#if NCRYPTO_USE_LEGACY_KEY_TYPES
+ // Decoding an ECPrivateKey without publicKey reconstructs the public point
+ // but retains a flag that omits it from subsequent encodings.
+ EC_KEY_set_enc_flags(ec.get(),
+ EC_KEY_get_enc_flags(ec.get()) & ~EC_PKEY_NO_PUBKEY);
+#endif
+ auto export_key = EVPKeyPointer::New();
+ if (!export_key || !export_key.set(ec)) return {};
+ auto encoded = export_key.writePrivateKey({});
+ if (!encoded) return {};
+ return std::move(encoded.value);
+}
+
DataPointer Ec::TryExportPublic(const EVPKeyPointer& key,
point_conversion_form_t form) {
if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {};
diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h
index 4649c514372..30dccfc0c79 100644
--- a/deps/ncrypto/ncrypto.h
+++ b/deps/ncrypto/ncrypto.h
@@ -778,6 +778,7 @@ class Ec final {
static DataPointer TryExportPublic(const EVPKeyPointer& key,
point_conversion_form_t form);
static DataPointer ExportPrivate(const EVPKeyPointer& key);
+ static BIOPointer ExportPrivatePkcs8(const EVPKeyPointer& key);
static bool GetKeyComponents(const EVPKeyPointer& key,
BignumPointer* x,
BignumPointer* y,
diff --git a/lib/internal/crypto/ec.js b/lib/internal/crypto/ec.js
index 6f566569a25..bb857be74c8 100644
--- a/lib/internal/crypto/ec.js
+++ b/lib/internal/crypto/ec.js
@@ -121,7 +121,7 @@ function ecExportKey(key, format) {
}
case kWebCryptoKeyFormatPKCS8: {
return TypedArrayPrototypeGetBuffer(
- handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
+ handle.exportECPrivatePkcs8());
}
default:
return undefined;
diff --git a/src/crypto/crypto_keys.cc b/src/crypto/crypto_keys.cc
index f787c97eb7f..580f9ac009d 100644
--- a/src/crypto/crypto_keys.cc
+++ b/src/crypto/crypto_keys.cc
@@ -1144,6 +1144,8 @@ Local<Function> KeyObjectHandle::Initialize(Environment* env) {
isolate, templ, "exportECPublicRaw", ExportECPublicRaw);
SetProtoMethodNoSideEffect(
isolate, templ, "exportECPrivateRaw", ExportECPrivateRaw);
+ SetProtoMethodNoSideEffect(
+ isolate, templ, "exportECPrivatePkcs8", ExportECPrivatePkcs8);
SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail);
SetProtoMethod(isolate, templ, "equals", Equals);
@@ -1167,6 +1169,7 @@ void KeyObjectHandle::RegisterExternalReferences(
registry->Register(RawSeed);
registry->Register(ExportECPublicRaw);
registry->Register(ExportECPrivateRaw);
+ registry->Register(ExportECPrivatePkcs8);
registry->Register(GetKeyDetail);
registry->Register(Equals);
}
@@ -1583,6 +1586,24 @@ void KeyObjectHandle::ExportECPrivateRaw(
.FromMaybe(Local<Value>()));
}
+void KeyObjectHandle::ExportECPrivatePkcs8(
+ const FunctionCallbackInfo<Value>& args) {
+ Environment* env = Environment::GetCurrent(args);
+ KeyObjectHandle* key;
+ ASSIGN_OR_RETURN_UNWRAP(&key, args.This());
+ const KeyObjectData& data = key->Data();
+ CHECK_EQ(data.GetKeyType(), kKeyTypePrivate);
+ Mutex::ScopedLock lock(data.mutex());
+ auto encoded = ncrypto::Ec::ExportPrivatePkcs8(data.GetAsymmetricKey());
+ if (!encoded) {
+ return THROW_ERR_CRYPTO_OPERATION_FAILED(env,
+ "Failed to export EC private key");
+ }
+ const EVPKeyPointer::PrivateKeyEncodingConfig config;
+ args.GetReturnValue().Set(
+ ToV8Value(env, encoded, config).FromMaybe(Local<Value>()));
+}
+
void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
KeyObjectHandle* key;
diff --git a/src/crypto/crypto_keys.h b/src/crypto/crypto_keys.h
index 9f3b1cce091..cb7209d0835 100644
--- a/src/crypto/crypto_keys.h
+++ b/src/crypto/crypto_keys.h
@@ -181,6 +181,8 @@ class KeyObjectHandle : public BaseObject {
const v8::FunctionCallbackInfo<v8::Value>& args);
static void ExportECPrivateRaw(
const v8::FunctionCallbackInfo<v8::Value>& args);
+ static void ExportECPrivatePkcs8(
+ const v8::FunctionCallbackInfo<v8::Value>& args);
static void RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args);
v8::MaybeLocal<v8::Value> ExportSecretKey() const;
diff --git a/test/parallel/test-webcrypto-ec-pkcs8-public-key.js b/test/parallel/test-webcrypto-ec-pkcs8-public-key.js
new file mode 100644
index 00000000000..e88ac624325
--- /dev/null
+++ b/test/parallel/test-webcrypto-ec-pkcs8-public-key.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+ common.skip('missing crypto');
+
+const assert = require('assert');
+const { createPrivateKey, KeyObject } = require('crypto');
+const fixtures = require('../common/fixtures');
+const { subtle } = globalThis.crypto;
+
+function der(tag, ...parts) {
+ const body = Buffer.concat(parts);
+ const length = body.length < 128 ? [body.length] : [0x81, body.length];
+ return Buffer.concat([Buffer.from([tag, ...length]), body]);
+}
+
+(async () => {
+ for (const [curve, oid] of [
+ ['p256', '06082a8648ce3d030107'],
+ ['p384', '06052b81040022'],
+ ['p521', '06052b81040023'],
+ ]) {
+ const privateKey = createPrivateKey(fixtures.readKey(`ec_${curve}_private.pem`));
+ const expected = privateKey.export({ type: 'pkcs8', format: 'der' });
+ const jwk = privateKey.export({ format: 'jwk' });
+ const curveOid = Buffer.from(oid, 'hex');
+ const algorithmIdentifier = der(
+ 0x30, Buffer.from('06072a8648ce3d0201', 'hex'), curveOid);
+
+ for (const includeParameters of [false, true]) {
+ const ecPrivateKey = der(
+ 0x30, Buffer.from('020101', 'hex'), der(0x04, Buffer.from(jwk.d, 'base64url')),
+ includeParameters ? der(0xa0, curveOid) : Buffer.alloc(0));
+ const privateOnly = der(
+ 0x30, Buffer.from('020100', 'hex'), algorithmIdentifier, der(0x04, ecPrivateKey));
+ for (const name of ['ECDSA', 'ECDH']) {
+ const key = await subtle.importKey(
+ 'pkcs8', privateOnly, { name, namedCurve: jwk.crv }, true,
+ name === 'ECDSA' ? ['sign'] : ['deriveBits']);
+ const original = KeyObject.from(key).export({ type: 'pkcs8', format: 'der' });
+ const actual = Buffer.from(await subtle.exportKey('pkcs8', key));
+ assert.deepStrictEqual(actual, expected);
+ assert.deepStrictEqual(
+ KeyObject.from(key).export({ type: 'pkcs8', format: 'der' }), original);
+ }
+ }
+ }
+})().then(common.mustCall());
diff --git a/typings/internalBinding/crypto.d.ts b/typings/internalBinding/crypto.d.ts
index 306e5738302..af6455f886f 100644
--- a/typings/internalBinding/crypto.d.ts
+++ b/typings/internalBinding/crypto.d.ts
@@ -583,6 +583,7 @@ declare namespace InternalCryptoBinding {
getAsymmetricKeyType(): string | undefined;
getSymmetricKeySize(): number;
checkEcKeyData(): boolean;
+ exportECPrivatePkcs8(): Buffer;
}
interface NativeKeyObject {