Commit 554dd55eb86 for nodejs
commit 554dd55eb86974fdbc9c84c7e006409df943acd2
Author: Filip Skokan <panva.ip@gmail.com>
Date: Thu Oct 1 08:43:05 2026 +0200
crypto: isolate provider EC PKCS8 encoding
OpenSSL's provider ec_pki_priv_to_der() temporarily changes EC_KEY
encoding flags on the input key. Concurrent PKCS8 exports can emit
inconsistent DER or affect a concurrent SEC1 export. The old Node key
mutex never covered this path.
Encode provider EC and SM2 keys through EVP_PKEY_dup(), preserving
encoding flags, point conversion form, parameters, and provider.
Legacy OpenSSL and BoringSSL already encode using local flags.
Restore concurrent KeyObject PKCS8 DER assertions and cover PEM and
SEC1 exports, including ECPrivateKey inputs without a public point.
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66413
Reviewed-By: James M Snell <jasnell@gmail.com>
diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 44b74e13fd2..1dbb6f43418 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -4401,11 +4401,26 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
break;
}
case PKEncodingType::PKCS8: {
+ EVP_PKEY* export_key = get();
+#if NCRYPTO_USE_OPENSSL3_PROVIDER
+ // OpenSSL's provider EC PKCS8 encoders temporarily change encoding flags.
+ // Use an independent key so concurrent exports do not change the source.
+ EVPKeyPointer key_copy;
+ if ((isA(KeyAlgorithm::EC) || isA(KeyAlgorithm::SM2)) &&
+ EVP_PKEY_get0_provider(get()) != nullptr) {
+ key_copy.reset(EVP_PKEY_dup(get()));
+ if (!key_copy) {
+ return Result<BIOPointer, bool>(false,
+ mark_pop_error_on_return.peekError());
+ }
+ export_key = key_copy.get();
+ }
+#endif
switch (config.format) {
case PKFormatType::PEM: {
// Encode PKCS#8 as PEM.
err = PEM_write_bio_PKCS8PrivateKey(bio.get(),
- get(),
+ export_key,
config.cipher,
passphrase.data,
passphrase.len,
@@ -4415,7 +4430,7 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
}
case PKFormatType::DER: {
err = i2d_PKCS8PrivateKey_bio(bio.get(),
- get(),
+ export_key,
config.cipher,
passphrase.data,
passphrase.len,
diff --git a/test/parallel/test-crypto-key-reuse-concurrent.js b/test/parallel/test-crypto-key-reuse-concurrent.js
index ae72fe7f84b..6c16fb88f0d 100644
--- a/test/parallel/test-crypto-key-reuse-concurrent.js
+++ b/test/parallel/test-crypto-key-reuse-concurrent.js
@@ -23,6 +23,8 @@ const signAsync = promisify(sign);
const verifyAsync = promisify(verify);
const ecdsa = { name: 'ECDSA', hash: 'SHA-256' };
const pkcs8 = { type: 'pkcs8', format: 'der' };
+const pkcs8Pem = { type: 'pkcs8', format: 'pem' };
+const sec1 = { type: 'sec1', format: 'der' };
const spki = { type: 'spki', format: 'der' };
const iterations = 16;
@@ -33,6 +35,8 @@ async function exercise({ keys, peer, expected }) {
key: expected.originalPrivate, ...pkcs8,
});
const referencePublic = createPublicKey({ key: expected.public, ...spki });
+ const referencePem = referencePrivate.export(pkcs8Pem);
+ const referenceSec1 = referencePrivate.export(sec1);
for (let i = 0; i < iterations; i++) {
const data = Buffer.from(`shared key operation ${i}`);
@@ -103,6 +107,10 @@ async function exercise({ keys, peer, expected }) {
}), Buffer.from(expected.compressedPublic));
assert.deepStrictEqual(
publicKey.export(spki), Buffer.from(expected.public));
+ assert.deepStrictEqual(
+ privateKey.export(pkcs8), Buffer.from(expected.originalPrivate));
+ assert.strictEqual(privateKey.export(pkcs8Pem), referencePem);
+ assert.deepStrictEqual(privateKey.export(sec1), referenceSec1);
if (keys.ecdsaPrivate === undefined) {
assert.deepStrictEqual(diffieHellman({
@@ -184,12 +192,12 @@ if (workerData?.sharedKeyTest) {
Atomics.notify(barrier, 0);
await Promise.all([exercise(data), ...exited]);
- // Ordinary PKCS8 encoding temporarily changes EC encoding flags in some
- // OpenSSL versions, so compare only after all shared-key users finish.
- // WebCrypto export must add the public point on a copy and leave the
- // original key's encoding flags unchanged.
+ // PKCS8 export must leave the source encoding flags unchanged, including
+ // whether SEC1 includes parameters and whether the public point is omitted.
assert.deepStrictEqual(
privateKey.export(pkcs8), Buffer.from(expected.originalPrivate));
+ assert.deepStrictEqual(privateKey.export(sec1),
+ createPrivateKey({ key: input, ...pkcs8 }).export(sec1));
}
(async () => {