Commit f39899dc74f for nodejs

commit f39899dc74fb2da8e266c3951f92316d7fc4414c
Author: Brad Anderson <brad@sankatygroup.com>
Date:   Mon Nov 9 13:52:14 2026 -0500

    crypto: fix ncrypto provider behavior

    Honor requested primality check counts with provider-backed OpenSSL.

    Support empty passphrases for encrypted SEC1 PEM output.

    Assisted-by: Codex
    Signed-off-by: Brad Anderson <brad@sankatygroup.com>
    PR-URL: https://github.com/nodejs/node/pull/67039
    Reviewed-By: Filip Skokan <panva.ip@gmail.com>

diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 7279e893194..15a1bd2de9d 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -724,6 +724,8 @@ BignumPointer BignumPointer::clone() {
 int BignumPointer::isPrime(int nchecks,
                            BignumPointer::PrimeCheckCallback innerCb) const {
   BignumCtxPointer ctx(BN_CTX_new());
+  if (!ctx) [[unlikely]]
+    return -1;
   BignumGenCallbackPointer cb(nullptr);
   if (innerCb != nullptr) {
     cb = BignumGenCallbackPointer(BN_GENCB_new());
@@ -741,7 +743,17 @@ int BignumPointer::isPrime(int nchecks,
         &innerCb);
   }
 #if NCRYPTO_USE_OPENSSL_PROVIDER
-  return BN_check_prime(get(), ctx.get(), cb.get());
+  const int minimum_checks = GetBitCount(get()) > 2048 ? 128 : 64;
+  const int repetitions =
+      nchecks > minimum_checks ? 1 + (nchecks - 1) / minimum_checks : 1;
+  for (int i = 0; i < repetitions; i++) {
+    // BN_check_prime() may return without invoking the callback (e.g. small
+    // primes resolved by trial division), so check cancellation here too.
+    if (i > 0 && innerCb != nullptr && !innerCb(1, -1)) return -1;
+    const int result = BN_check_prime(get(), ctx.get(), cb.get());
+    if (result != 1) return result;
+  }
+  return 1;
 #elif NCRYPTO_USE_BORINGSSL
   int is_probably_prime = 0;
   if (BN_primality_test(
@@ -4496,6 +4508,9 @@ int WriteDERView(const void* x, unsigned char** out) {

 bool WriteEncryptedTraditionalPEM(BIO* bio,
                                   const EVP_PKEY* pkey,
+                                  int selection,
+                                  const char* structure,
+                                  const char* label,
                                   const EVP_CIPHER* cipher,
                                   Buffer<char> passphrase) {
   if (passphrase.len > static_cast<size_t>(INT_MAX)) return false;
@@ -4503,7 +4518,7 @@ bool WriteEncryptedTraditionalPEM(BIO* bio,
   unsigned char* der = nullptr;
   size_t der_len = 0;
   OSSLEncoderCtxPointer ctx(OSSL_ENCODER_CTX_new_for_pkey(
-      pkey, OSSL_KEYMGMT_SELECT_KEYPAIR, "DER", "pkcs1", nullptr));
+      pkey, selection, "DER", structure, nullptr));
   if (!ctx) return false;

   const int result = OSSL_ENCODER_to_data(ctx.get(), &der, &der_len);
@@ -4513,7 +4528,7 @@ bool WriteEncryptedTraditionalPEM(BIO* bio,
   DERView der_view{der_storage.get<const unsigned char>(), der_len};
   return PEM_ASN1_write_bio(
              WriteDERView,
-             PEM_STRING_RSA,
+             label,
              bio,
              &der_view,
              cipher,
@@ -4727,8 +4742,13 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
       const EVP_CIPHER* cipher =
           config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
       if (cipher != nullptr && passphrase.len == 0) {
-        err =
-            !WriteEncryptedTraditionalPEM(bio.get(), get(), cipher, passphrase);
+        err = !WriteEncryptedTraditionalPEM(bio.get(),
+                                            get(),
+                                            OSSL_KEYMGMT_SELECT_KEYPAIR,
+                                            "pkcs1",
+                                            PEM_STRING_RSA,
+                                            cipher,
+                                            passphrase);
       } else {
         err = !WriteEncodedPKey(bio.get(),
                                 get(),
@@ -4821,13 +4841,23 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
 #if NCRYPTO_USE_OPENSSL_PROVIDER
       const EVP_CIPHER* cipher =
           config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
-      err = !WriteEncodedPKey(bio.get(),
-                              get(),
-                              OSSL_KEYMGMT_SELECT_ALL,
-                              config.format,
-                              "type-specific",
-                              cipher,
-                              passphrase);
+      if (cipher != nullptr && passphrase.len == 0) {
+        err = !WriteEncryptedTraditionalPEM(bio.get(),
+                                            get(),
+                                            OSSL_KEYMGMT_SELECT_ALL,
+                                            "type-specific",
+                                            PEM_STRING_ECPRIVATEKEY,
+                                            cipher,
+                                            passphrase);
+      } else {
+        err = !WriteEncodedPKey(bio.get(),
+                                get(),
+                                OSSL_KEYMGMT_SELECT_ALL,
+                                config.format,
+                                "type-specific",
+                                cipher,
+                                passphrase);
+      }
 #elif NCRYPTO_USE_BORINGSSL
       EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get());
       if (ec == nullptr) return Result<BIOPointer, bool>(false);
diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index e23a3b82df0..6a1ccf09c54 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -491,6 +491,82 @@ TEST(NodeCrypto, RsaPrimeProduct) {
   EXPECT_FALSE(rsa.checkPrimeProduct());
 }

+#if NCRYPTO_USE_OPENSSL_PROVIDER
+namespace {
+constexpr int kMinimumPrimeChecks = 64;
+
+bool CheckMersennePrime(int exponent, int requested_checks) {
+  auto candidate = ncrypto::BignumPointer::New();
+  if (!candidate || BN_set_bit(candidate.get(), exponent) != 1 ||
+      BN_sub_word(candidate.get(), 1) != 1) {
+    return false;
+  }
+
+  int completed_checks = 0;
+  const int result = candidate.isPrime(
+      requested_checks, [&completed_checks](int event, int iteration) {
+        if (event == 1 && iteration >= 0) completed_checks++;
+        return true;
+      });
+  return result == 1 && completed_checks >= requested_checks;
+}
+}  // namespace
+
+TEST(NodeCrypto, BignumPrimeChecksHonorRequestedCount) {
+  EXPECT_TRUE(CheckMersennePrime(127, kMinimumPrimeChecks + 1));
+  EXPECT_TRUE(CheckMersennePrime(2203, 129));
+}
+
+TEST(NodeCrypto, BignumPrimeCheckCancellation) {
+  auto candidate = ncrypto::BignumPointer::New();
+  ASSERT_TRUE(candidate);
+  ASSERT_EQ(BN_set_bit(candidate.get(), 127), 1);
+  ASSERT_EQ(BN_sub_word(candidate.get(), 1), 1);
+
+  int invocations = 0;
+  int completed_checks = 0;
+  bool valid_events = true;
+  const int result =
+      candidate.isPrime(kMinimumPrimeChecks + 1, [&](int event, int iteration) {
+        if (event != 1) {
+          valid_events = false;
+          return false;
+        }
+        if (iteration == -1) {
+          invocations++;
+          return true;
+        }
+        if (iteration != completed_checks % kMinimumPrimeChecks) {
+          valid_events = false;
+          return false;
+        }
+        if (completed_checks == kMinimumPrimeChecks) return false;
+        completed_checks++;
+        return true;
+      });
+  EXPECT_EQ(result, -1);
+  EXPECT_TRUE(valid_events);
+  EXPECT_EQ(invocations, 3);
+  EXPECT_EQ(completed_checks, kMinimumPrimeChecks);
+  ERR_clear_error();
+}
+
+TEST(NodeCrypto, BignumPrimeCheckCancellationSmallPrime) {
+  // 311 is resolved by trial division, so OpenSSL never invokes the callback.
+  auto candidate = ncrypto::BignumPointer::New();
+  ASSERT_TRUE(candidate);
+  ASSERT_EQ(BN_set_word(candidate.get(), 311), 1);
+
+  int invocations = 0;
+  const int result = candidate.isPrime(INT_MAX, [&](int, int) {
+    invocations++;
+    return false;
+  });
+  EXPECT_EQ(result, -1);
+  EXPECT_EQ(invocations, 1);
+}
+#endif
+
 TEST(NodeCrypto, EcKeyComponents) {
   ncrypto::ClearErrorOnReturn clear_errors;
   ncrypto::BignumPointer x;
diff --git a/test/parallel/test-crypto-key-export-sec1-passphrase.js b/test/parallel/test-crypto-key-export-sec1-passphrase.js
new file mode 100644
index 00000000000..f406151607e
--- /dev/null
+++ b/test/parallel/test-crypto-key-export-sec1-passphrase.js
@@ -0,0 +1,46 @@
+'use strict';
+
+const common = require('../common');
+if (!common.hasCrypto)
+  common.skip('missing crypto');
+
+const { hasFIPS, isBoringSSL } = require('../common/crypto');
+
+if (isBoringSSL)
+  common.skip('BoringSSL does not use OpenSSL provider encoders');
+if (hasFIPS(3))
+  common.skip('SEC1 private key encoding is not supported in FIPS mode');
+
+const assert = require('assert');
+const {
+  createPrivateKey,
+  generateKeyPairSync,
+} = require('crypto');
+
+const { privateKey } = generateKeyPairSync('ec', {
+  namedCurve: 'prime256v1',
+});
+
+for (const passphrase of [
+  Buffer.alloc(0),
+  Buffer.from([0x62, 0x69, 0x00, 0x6e, 0x61, 0x72, 0x79]),
+]) {
+  const pem = privateKey.export({
+    type: 'sec1',
+    format: 'pem',
+    cipher: 'aes-128-cbc',
+    passphrase,
+  });
+  assert.match(pem.toString(), /^-----BEGIN EC PRIVATE KEY-----\n/);
+  assert.strictEqual(createPrivateKey({ key: pem, passphrase })
+    .asymmetricKeyType, 'ec');
+  // A wrong CBC password can occasionally yield valid padding, in which case
+  // ASN.1 decoding fails instead.
+  assert.throws(() => createPrivateKey({
+    key: pem,
+    passphrase: Buffer.from('wrong'),
+  }), { code: /^ERR_OSSL_(BAD_DECRYPT|UNSUPPORTED)$/ });
+  assert.throws(() => createPrivateKey({ key: pem }), {
+    code: 'ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED',
+  });
+}