Commit 0fc31021c1e for nodejs

commit 0fc31021c1e8f0005fec1451c90df7620a2f6f3a
Author: Filip Skokan <panva.ip@gmail.com>
Date:   Tue Oct 27 13:35:30 2026 +0100

    crypto: preserve errors during X.509 issuer checks

    X509View::isIssuedBy() clears the caller's OpenSSL error queue on
    entry and exit, including when either certificate is missing.

    Use a mark/pop scope to discard errors from the issuer check without
    losing earlier diagnostics. Cover successful checks, mismatches,
    malformed extensions, and null inputs with empty and populated queues.

    Assisted-by: Codex
    Signed-off-by: Filip Skokan <panva.ip@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66898
    Reviewed-By: Tim Perry <pimterry@gmail.com>

diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc
index 5c20b482878..a28b9cd209e 100644
--- a/deps/ncrypto/ncrypto.cc
+++ b/deps/ncrypto/ncrypto.cc
@@ -1595,7 +1595,7 @@ bool X509View::isCA() const {
 }

 bool X509View::isIssuedBy(const X509View& issuer) const {
-  ClearErrorOnReturn clearErrorOnReturn;
+  MarkPopErrorOnReturn mark_pop_error_on_return;
   if (cert_ == nullptr || issuer.cert_ == nullptr) return false;
   return X509_check_issued(const_cast<X509*>(issuer.cert_),
                            const_cast<X509*>(cert_)) == X509_V_OK;
diff --git a/test/cctest/test_node_crypto.cc b/test/cctest/test_node_crypto.cc
index 7658e1810cd..45d5b7facc3 100644
--- a/test/cctest/test_node_crypto.cc
+++ b/test/cctest/test_node_crypto.cc
@@ -1025,6 +1025,105 @@ TEST(NodeCrypto, X509FormattingPreservesUnrelatedErrors) {
   ERR_clear_error();
 }

+TEST(NodeCrypto, X509IssuerChecksPreserveUnrelatedErrors) {
+  ncrypto::ClearErrorOnReturn clear_errors;
+  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::RSA);
+  ASSERT_TRUE(ctx);
+  ASSERT_TRUE(ctx.initForKeygen());
+  ASSERT_TRUE(ctx.setRsaKeygenBits(2048));
+  EVP_PKEY* raw = nullptr;
+  ASSERT_EQ(EVP_PKEY_keygen(ctx.get(), &raw), 1);
+  EVPKeyPointer key(raw);
+
+  ncrypto::X509Pointer cert(X509_new());
+  ASSERT_TRUE(cert);
+  ASSERT_EQ(X509_set_version(cert.get(), 2), 1);
+  ASSERT_EQ(ASN1_INTEGER_set(X509_get_serialNumber(cert.get()), 1), 1);
+  ASSERT_EQ(
+      ASN1_TIME_set_string(X509_getm_notBefore(cert.get()), "20260101000000Z"),
+      1);
+  ASSERT_EQ(
+      ASN1_TIME_set_string(X509_getm_notAfter(cert.get()), "20270101000000Z"),
+      1);
+  X509NamePointer name(X509_NAME_new());
+  ASSERT_TRUE(name);
+  ASSERT_EQ(X509_NAME_add_entry_by_txt(
+                name.get(),
+                "CN",
+                MBSTRING_ASC,
+                reinterpret_cast<const unsigned char*>("issuer"),
+                -1,
+                -1,
+                0),
+            1);
+  ASSERT_EQ(X509_set_subject_name(cert.get(), name.get()), 1);
+  ASSERT_EQ(X509_set_issuer_name(cert.get(), name.get()), 1);
+  ASSERT_EQ(X509_set_pubkey(cert.get(), key.get()), 1);
+  ASSERT_GT(X509_sign(cert.get(), key.get(), EVP_sha256()), 0);
+
+  X509NamePointer mismatch_name(X509_NAME_dup(name.get()));
+  ASSERT_TRUE(mismatch_name);
+  ASSERT_EQ(X509_NAME_add_entry_by_txt(
+                mismatch_name.get(),
+                "OU",
+                MBSTRING_ASC,
+                reinterpret_cast<const unsigned char*>("other"),
+                -1,
+                -1,
+                0),
+            1);
+
+  ncrypto::DeleteFnPtr<ASN1_OCTET_STRING, ASN1_OCTET_STRING_free> invalid_value(
+      ASN1_OCTET_STRING_new());
+  ASSERT_TRUE(invalid_value);
+  const unsigned char truncated_sequence[] = {0x30};
+  ASSERT_EQ(
+      ASN1_OCTET_STRING_set(
+          invalid_value.get(), truncated_sequence, sizeof(truncated_sequence)),
+      1);
+  X509ExtensionPointer invalid_constraints(X509_EXTENSION_create_by_NID(
+      nullptr, NID_basic_constraints, 0, invalid_value.get()));
+  ASSERT_TRUE(invalid_constraints);
+
+  for (const bool seed_error : {true, false}) {
+    // Reparse each issuer so extension-cache failures are exercised each time.
+    auto matching_issuer = cert.view().clone();
+    auto mismatch = cert.view().clone();
+    auto malformed = cert.view().clone();
+    ASSERT_TRUE(matching_issuer);
+    ASSERT_TRUE(mismatch);
+    ASSERT_TRUE(malformed);
+    ASSERT_EQ(X509_set_subject_name(mismatch.get(), mismatch_name.get()), 1);
+    ASSERT_EQ(X509_add_ext(malformed.get(), invalid_constraints.get(), -1), 1);
+
+    const auto check = [seed_error](ncrypto::X509View subject,
+                                    ncrypto::X509View issuer,
+                                    bool issued) {
+      ERR_clear_error();
+      if (seed_error) {
+#if NCRYPTO_USE_OPENSSL_PROVIDER
+        ERR_raise(ERR_LIB_USER, 1);
+#elif NCRYPTO_USE_BORINGSSL
+        ERR_put_error(ERR_LIB_USER, 0, 1, __FILE__, __LINE__);
+#else
+#error "Unsupported crypto backend"
+#endif
+      }
+      const auto expected = ERR_peek_error();
+      if (seed_error) ASSERT_NE(expected, 0UL);
+      EXPECT_EQ(subject.isIssuedBy(issuer), issued);
+      EXPECT_EQ(ERR_get_error(), expected);
+      EXPECT_EQ(ERR_get_error(), 0UL);
+    };
+
+    check(cert.view(), matching_issuer.view(), true);
+    check(cert.view(), mismatch.view(), false);
+    check(cert.view(), malformed.view(), false);
+    check({}, matching_issuer.view(), false);
+    check(cert.view(), {}, false);
+  }
+}
+
 TEST(NodeCrypto, X509KeyVisitorsHandleMissingPublicKey) {
   ncrypto::X509Pointer cert(X509_new());
   ASSERT_TRUE(cert);