Commit 3dbbdd07987 for nodejs

commit 3dbbdd07987496209f15a1184e289873eff7a3ed
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Wed Sep 30 14:05:15 2026 +0200

    crypto: remove KEM key locking

    ncrypto::KEM creates a new EVP_PKEY_CTX per operation. Parameters,
    entropy, scratch buffers, and outputs are private to each call. ML-KEM
    expansion finishes during import, and operations read an immutable key.
    EC and X25519 retain read-only recipient keys. RSA synchronizes mutable
    caches internally.

    Remove the outer locks serializing jobs that share a KeyObject or
    CryptoKey. Cover cold keys and worker clones across RSA, EC, X25519, and
    ML-KEM, checking every resulting shared secret.

    Refs: https://docs.openssl.org/3.5/man7/openssl-threads/
    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/src/crypto/crypto_kem.cc b/src/crypto/crypto_kem.cc
index 726912b57f2..7d3f309157d 100644
--- a/src/crypto/crypto_kem.cc
+++ b/src/crypto/crypto_kem.cc
@@ -117,7 +117,6 @@ KEMEncapsulateJob::KEMEncapsulateJob(Environment* env,
 void KEMEncapsulateJob::DoThreadPoolWork() {
   ncrypto::ClearErrorOnReturn clear_error_on_return;
   AdditionalParams* params = CryptoJob<KEMEncapsulateTraits>::params();
-  Mutex::ScopedLock lock(params->key.mutex());
   auto result = ncrypto::KEM::Encapsulate(params->key.GetAsymmetricKey());
   if (result) {
     out_.emplace();
@@ -223,7 +222,6 @@ bool KEMDecapsulateTraits::DeriveBits(Environment* env,
                                       ByteSource* out,
                                       CryptoJobMode mode,
                                       CryptoErrorStore* errors) {
-  Mutex::ScopedLock lock(params.key.mutex());
   const auto& private_key = params.key.GetAsymmetricKey();

   return DoKEMDecapsulate(
diff --git a/test/parallel/test-crypto-kem-shared-key.js b/test/parallel/test-crypto-kem-shared-key.js
new file mode 100644
index 00000000000..da28d5b4134
--- /dev/null
+++ b/test/parallel/test-crypto-kem-shared-key.js
@@ -0,0 +1,140 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const assert = require('assert');
+const {
+  createPrivateKey,
+  createPublicKey,
+  encapsulate,
+  decapsulate,
+} = require('crypto');
+const { once } = require('events');
+const { promisify } = require('util');
+const { Worker, parentPort, workerData } = require('worker_threads');
+const { hasOpenSSL, hasFIPS, isBoringSSL } = require('../common/crypto');
+const fixtures = require('../common/fixtures');
+const { subtle } = globalThis.crypto;
+
+if (!hasOpenSSL(3) && !isBoringSSL)
+  common.skip('requires OpenSSL >= 3 or BoringSSL');
+
+const encapsulateAsync = promisify(encapsulate);
+const decapsulateAsync = promisify(decapsulate);
+
+async function encapsulateKeys(keys) {
+  const operations = Array.from({ length: 4 }, () =>
+    encapsulateAsync(keys.publicKey));
+  operations.push(encapsulate(keys.publicKey));
+  if (keys.publicCryptoKey) {
+    operations.push(subtle.encapsulateBits('ML-KEM-768', keys.publicCryptoKey));
+  }
+
+  const results = await Promise.all(operations);
+  for (const { sharedKey, ciphertext } of results) {
+    assert.strictEqual(sharedKey.byteLength, keys.sharedSecretLength);
+    assert.strictEqual(ciphertext.byteLength, keys.ciphertextLength);
+  }
+  return results;
+}
+
+async function decapsulateKeys(keys, results) {
+  const operations = results.map(common.mustCall(async ({ sharedKey, ciphertext }) => {
+    const result = await decapsulateAsync(keys.privateKey, ciphertext);
+    assert.deepStrictEqual(result, Buffer.from(sharedKey));
+  }, results.length));
+
+  for (const { sharedKey, ciphertext } of results) {
+    assert.deepStrictEqual(
+      decapsulate(keys.privateKey, ciphertext), Buffer.from(sharedKey));
+    if (keys.privateCryptoKey) {
+      operations.push(subtle.decapsulateBits(
+        'ML-KEM-768', keys.privateCryptoKey, ciphertext,
+      ).then((result) => {
+        assert.deepStrictEqual(Buffer.from(result), Buffer.from(sharedKey));
+      }));
+    }
+  }
+
+  await Promise.all(operations);
+}
+
+function waitForStart(barrier, phase) {
+  parentPort.postMessage(phase);
+  Atomics.wait(barrier, phase, 0);
+}
+
+async function runWorker() {
+  const barrier = new Int32Array(workerData.barrier);
+  waitForStart(barrier, 0);
+  const results = await encapsulateKeys(workerData.keys);
+  waitForStart(barrier, 1);
+  await decapsulateKeys(workerData.keys, results);
+}
+
+async function testKeys(keys) {
+  const barrier = new Int32Array(new SharedArrayBuffer(8));
+  const workers = Array.from({ length: 3 }, () => new Worker(__filename, {
+    workerData: { test: 'kem-shared-key', keys, barrier: barrier.buffer },
+  }));
+  const exits = workers.map((worker) => once(worker, 'exit'));
+
+  // Synchronize each phase before the backing keys have been used for it.
+  const ready = await Promise.all(
+    workers.map((worker) => once(worker, 'message')));
+  for (const [phase] of ready)
+    assert.strictEqual(phase, 0);
+  const decapsulateReady = workers.map((worker) => once(worker, 'message'));
+  Atomics.store(barrier, 0, 1);
+  Atomics.notify(barrier, 0);
+  const results = await encapsulateKeys(keys);
+
+  for (const [phase] of await Promise.all(decapsulateReady))
+    assert.strictEqual(phase, 1);
+  Atomics.store(barrier, 1, 1);
+  Atomics.notify(barrier, 1);
+  await decapsulateKeys(keys, results);
+
+  for (const [code] of await Promise.all(exits))
+    assert.strictEqual(code, 0);
+}
+
+async function runMain() {
+  const types = [];
+  if (hasOpenSSL(3)) {
+    types.push(['rsa', 'rsa', 256, 256]);
+  }
+  if (hasOpenSSL(3, 2) && !hasFIPS(3)) {
+    types.push(['ec', 'ec_p256', 32, 65], ['x25519', 'x25519', 32, 32]);
+  }
+  if (hasOpenSSL(3, 5) || isBoringSSL) {
+    types.push(['ml-kem-768', 'ml_kem_768', 32, 1088]);
+  }
+
+  for (const [type, prefix, sharedSecretLength, ciphertextLength] of types) {
+    const suffix = type === 'rsa' ? '_2048' : '';
+    const privateSuffix = type === 'ml-kem-768' ? '_seed_only' : suffix;
+    const publicKey = createPublicKey(fixtures.readKey(
+      `${prefix}_public${suffix}.pem`));
+    const privateKey = createPrivateKey(fixtures.readKey(
+      `${prefix}_private${privateSuffix}.pem`));
+    const keys = {
+      publicKey, privateKey, sharedSecretLength, ciphertextLength,
+    };
+    if (type === 'ml-kem-768') {
+      keys.publicCryptoKey = publicKey.toCryptoKey(
+        'ML-KEM-768', false, ['encapsulateBits']);
+      keys.privateCryptoKey = privateKey.toCryptoKey(
+        'ML-KEM-768', false, ['decapsulateBits']);
+    }
+    await testKeys(keys);
+  }
+}
+
+if (workerData?.test === 'kem-shared-key') {
+  runWorker().then(common.mustCall());
+} else {
+  runMain().then(common.mustCall());
+}