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 {