Skip to content

Commit 642f172

Browse files
panvanodejs-github-bot
authored andcommitted
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: #66237 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Aviv Keller <me@aviv.sh>
1 parent 090c6d7 commit 642f172

7 files changed

Lines changed: 93 additions & 1 deletion

File tree

‎deps/ncrypto/ncrypto.cc‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7384,6 +7384,24 @@ int Ec::getCurve() const {
73847384
return EC_GROUP_get_curve_name(getGroup());
73857385
}
73867386

7387+
BIOPointer Ec::ExportPrivatePkcs8(const EVPKeyPointer& key) {
7388+
MarkPopErrorOnReturn mark_pop_error_on_return;
7389+
if (!key || !key.isA(KeyAlgorithm::EC)) return {};
7390+
auto ec = ECKeyPointer(key).clone();
7391+
if (!ec) return {};
7392+
#if NCRYPTO_USE_LEGACY_KEY_TYPES
7393+
// Decoding an ECPrivateKey without publicKey reconstructs the public point
7394+
// but retains a flag that omits it from subsequent encodings.
7395+
EC_KEY_set_enc_flags(ec.get(),
7396+
EC_KEY_get_enc_flags(ec.get()) & ~EC_PKEY_NO_PUBKEY);
7397+
#endif
7398+
auto export_key = EVPKeyPointer::New();
7399+
if (!export_key || !export_key.set(ec)) return {};
7400+
auto encoded = export_key.writePrivateKey({});
7401+
if (!encoded) return {};
7402+
return std::move(encoded.value);
7403+
}
7404+
73877405
DataPointer Ec::TryExportPublic(const EVPKeyPointer& key,
73887406
point_conversion_form_t form) {
73897407
if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {};

‎deps/ncrypto/ncrypto.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,7 @@ class Ec final {
778778
static DataPointer TryExportPublic(const EVPKeyPointer& key,
779779
point_conversion_form_t form);
780780
static DataPointer ExportPrivate(const EVPKeyPointer& key);
781+
static BIOPointer ExportPrivatePkcs8(const EVPKeyPointer& key);
781782
static bool GetKeyComponents(const EVPKeyPointer& key,
782783
BignumPointer* x,
783784
BignumPointer* y,

‎lib/internal/crypto/ec.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ function ecExportKey(key, format) {
121121
}
122122
case kWebCryptoKeyFormatPKCS8: {
123123
return TypedArrayPrototypeGetBuffer(
124-
handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
124+
handle.exportECPrivatePkcs8());
125125
}
126126
default:
127127
return undefined;

‎src/crypto/crypto_keys.cc‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,8 @@ Local<Function> KeyObjectHandle::Initialize(Environment* env) {
11441144
isolate, templ, "exportECPublicRaw", ExportECPublicRaw);
11451145
SetProtoMethodNoSideEffect(
11461146
isolate, templ, "exportECPrivateRaw", ExportECPrivateRaw);
1147+
SetProtoMethodNoSideEffect(
1148+
isolate, templ, "exportECPrivatePkcs8", ExportECPrivatePkcs8);
11471149
SetProtoMethod(isolate, templ, "keyDetail", GetKeyDetail);
11481150
SetProtoMethod(isolate, templ, "equals", Equals);
11491151

@@ -1167,6 +1169,7 @@ void KeyObjectHandle::RegisterExternalReferences(
11671169
registry->Register(RawSeed);
11681170
registry->Register(ExportECPublicRaw);
11691171
registry->Register(ExportECPrivateRaw);
1172+
registry->Register(ExportECPrivatePkcs8);
11701173
registry->Register(GetKeyDetail);
11711174
registry->Register(Equals);
11721175
}
@@ -1583,6 +1586,24 @@ void KeyObjectHandle::ExportECPrivateRaw(
15831586
.FromMaybe(Local<Value>()));
15841587
}
15851588

1589+
void KeyObjectHandle::ExportECPrivatePkcs8(
1590+
const FunctionCallbackInfo<Value>& args) {
1591+
Environment* env = Environment::GetCurrent(args);
1592+
KeyObjectHandle* key;
1593+
ASSIGN_OR_RETURN_UNWRAP(&key, args.This());
1594+
const KeyObjectData& data = key->Data();
1595+
CHECK_EQ(data.GetKeyType(), kKeyTypePrivate);
1596+
Mutex::ScopedLock lock(data.mutex());
1597+
auto encoded = ncrypto::Ec::ExportPrivatePkcs8(data.GetAsymmetricKey());
1598+
if (!encoded) {
1599+
return THROW_ERR_CRYPTO_OPERATION_FAILED(env,
1600+
"Failed to export EC private key");
1601+
}
1602+
const EVPKeyPointer::PrivateKeyEncodingConfig config;
1603+
args.GetReturnValue().Set(
1604+
ToV8Value(env, encoded, config).FromMaybe(Local<Value>()));
1605+
}
1606+
15861607
void KeyObjectHandle::RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args) {
15871608
Environment* env = Environment::GetCurrent(args);
15881609
KeyObjectHandle* key;

‎src/crypto/crypto_keys.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ class KeyObjectHandle : public BaseObject {
181181
const v8::FunctionCallbackInfo<v8::Value>& args);
182182
static void ExportECPrivateRaw(
183183
const v8::FunctionCallbackInfo<v8::Value>& args);
184+
static void ExportECPrivatePkcs8(
185+
const v8::FunctionCallbackInfo<v8::Value>& args);
184186
static void RawSeed(const v8::FunctionCallbackInfo<v8::Value>& args);
185187

186188
v8::MaybeLocal<v8::Value> ExportSecretKey() const;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const assert = require('assert');
8+
const { createPrivateKey, KeyObject } = require('crypto');
9+
const fixtures = require('../common/fixtures');
10+
const { subtle } = globalThis.crypto;
11+
12+
function der(tag, ...parts) {
13+
const body = Buffer.concat(parts);
14+
const length = body.length < 128 ? [body.length] : [0x81, body.length];
15+
return Buffer.concat([Buffer.from([tag, ...length]), body]);
16+
}
17+
18+
(async () => {
19+
for (const [curve, oid] of [
20+
['p256', '06082a8648ce3d030107'],
21+
['p384', '06052b81040022'],
22+
['p521', '06052b81040023'],
23+
]) {
24+
const privateKey = createPrivateKey(fixtures.readKey(`ec_${curve}_private.pem`));
25+
const expected = privateKey.export({ type: 'pkcs8', format: 'der' });
26+
const jwk = privateKey.export({ format: 'jwk' });
27+
const curveOid = Buffer.from(oid, 'hex');
28+
const algorithmIdentifier = der(
29+
0x30, Buffer.from('06072a8648ce3d0201', 'hex'), curveOid);
30+
31+
for (const includeParameters of [false, true]) {
32+
const ecPrivateKey = der(
33+
0x30, Buffer.from('020101', 'hex'), der(0x04, Buffer.from(jwk.d, 'base64url')),
34+
includeParameters ? der(0xa0, curveOid) : Buffer.alloc(0));
35+
const privateOnly = der(
36+
0x30, Buffer.from('020100', 'hex'), algorithmIdentifier, der(0x04, ecPrivateKey));
37+
for (const name of ['ECDSA', 'ECDH']) {
38+
const key = await subtle.importKey(
39+
'pkcs8', privateOnly, { name, namedCurve: jwk.crv }, true,
40+
name === 'ECDSA' ? ['sign'] : ['deriveBits']);
41+
const original = KeyObject.from(key).export({ type: 'pkcs8', format: 'der' });
42+
const actual = Buffer.from(await subtle.exportKey('pkcs8', key));
43+
assert.deepStrictEqual(actual, expected);
44+
assert.deepStrictEqual(
45+
KeyObject.from(key).export({ type: 'pkcs8', format: 'der' }), original);
46+
}
47+
}
48+
}
49+
})().then(common.mustCall());

‎typings/internalBinding/crypto.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ declare namespace InternalCryptoBinding {
583583
getAsymmetricKeyType(): string | undefined;
584584
getSymmetricKeySize(): number;
585585
checkEcKeyData(): boolean;
586+
exportECPrivatePkcs8(): Buffer;
586587
}
587588

588589
interface NativeKeyObject {

0 commit comments

Comments
 (0)